Skip to content

Commit

Permalink
[Nanoleaf] New Channel: State (openhab#13746)
Browse files Browse the repository at this point in the history
* [Nanoleaf] New Channel: State

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

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

Signed-off-by: Jørgen Austvik <jaustvik@acm.org>
  • Loading branch information
austvik authored and borazslo committed Jan 8, 2023
1 parent 8e08b98 commit cd75cde
Show file tree
Hide file tree
Showing 31 changed files with 2,417 additions and 235 deletions.
14 changes: 12 additions & 2 deletions bundles/org.openhab.binding.nanoleaf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 |



Expand Down
Binary file modified bundles/org.openhab.binding.nanoleaf/doc/Layout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ public NanoleafHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
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;
Expand Down Expand Up @@ -101,12 +103,12 @@ public class NanoleafControllerHandler extends BaseBridgeHandler {
private static final int CONNECT_TIMEOUT = 10;

private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
private HttpClientFactory httpClientFactory;
private HttpClient httpClient;
private final HttpClientFactory httpClientFactory;
private final HttpClient httpClient;

private @Nullable HttpClient httpClientSSETouchEvent;
private @Nullable Request sseTouchjobRequest;
private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
private final List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
private PanelLayout previousPanelLayout = new PanelLayout();

private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
Expand Down Expand Up @@ -515,33 +517,30 @@ private synchronized void runTouchDetection() {
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);
localSSETouchjobRequest.onResponseContent((response, content) -> {
String s = StandardCharsets.UTF_8.decode(content).toString();
logger.debug("touch detected for controller {}", thing.getUID());
logger.trace("content {}", s);
Scanner eventContent = new Scanner(s);

while (eventContent.hasNextLine()) {
String line = eventContent.nextLine().trim();
if (line.startsWith("data:")) {
String json = line.substring(5).trim();

try {
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
handleTouchEvents(Objects.requireNonNull(touchEvents));
} catch (JsonSyntaxException e) {
logger.error("Couldn't parse touch event json {}", json);
try (Scanner eventContent = new Scanner(s)) {
while (eventContent.hasNextLine()) {
String line = eventContent.nextLine().trim();
if (line.startsWith("data:")) {
String json = line.substring(5).trim();

try {
TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
handleTouchEvents(Objects.requireNonNull(touchEvents));
} catch (JsonSyntaxException e) {
logger.error("Couldn't parse touch event json {}", json);
}
}
}
}

eventContent.close();
logger.debug("leaving touch onContent");
}).onResponseSuccess((response) -> {
logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response);
Expand Down Expand Up @@ -670,6 +669,7 @@ private void updateFromControllerInfo() throws NanoleafException {
updateProperties();
updateConfiguration();
updateLayout(controllerInfo.getPanelLayout());
updateState(controllerInfo.getPanelLayout());

for (NanoleafControllerListener controllerListener : controllerListeners) {
controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
Expand Down Expand Up @@ -711,6 +711,24 @@ private void updateProperties() {
}
}

private void updateState(PanelLayout panelLayout) {
ChannelUID stateChannel = new ChannelUID(getThing().getUID(), CHANNEL_STATE);

Bridge bridge = getThing();
List<Thing> things = bridge.getThings();
try {
LayoutSettings settings = new LayoutSettings(false, true, true, true);
byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings);
if (bytes.length > 0) {
updateState(stateChannel, new RawType(bytes, "image/png"));
}

previousPanelLayout = panelLayout;
} catch (IOException ioex) {
logger.warn("Failed to create state image", ioex);
}
}

private void updateLayout(PanelLayout panelLayout) {
ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT);
ThingHandlerCallback callback = getCallback();
Expand All @@ -726,10 +744,13 @@ private void updateLayout(PanelLayout panelLayout) {
return;
}

Bridge bridge = getThing();
List<Thing> things = bridge.getThings();
try {
byte[] bytes = NanoleafLayout.render(panelLayout);
LayoutSettings settings = new LayoutSettings(true, false, true, false);
byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings);
if (bytes.length > 0) {
updateState(CHANNEL_LAYOUT, new RawType(bytes, "image/png"));
updateState(layoutChannel, new RawType(bytes, "image/png"));
}

previousPanelLayout = panelLayout;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ public class NanoleafPanelHandler extends BaseThingHandler {

private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);

private HttpClient httpClient;
private final HttpClient httpClient;
// JSON parser for API responses
private final Gson gson = new Gson();

// holds current color data per panel
private Map<String, HSBType> panelInfo = new HashMap<>();
private final Map<String, HSBType> panelInfo = new HashMap<>();

private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
Expand Down Expand Up @@ -227,7 +227,7 @@ private void sendRenderedEffectCommand(Command command) throws NanoleafException
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) {
Expand All @@ -239,8 +239,8 @@ private void sendRenderedEffectCommand(Command command) throws NanoleafException
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));
}
Expand Down Expand Up @@ -288,6 +288,11 @@ public String getPanelID() {
return panelID;
}

public @Nullable HSBType getColor() {
String panelID = getPanelID();
return panelInfo.get(panelID);
}

private @Nullable HSBType getPanelColor() {
String panelID = getPanelID();

Expand Down Expand Up @@ -357,9 +362,9 @@ void parsePanelData(String panelID, NanoleafControllerConfig config, ContentResp
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nanoleaf.internal.layout;

import java.awt.Color;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;

/**
* Information to the drawing algorithm about which style to use and how to draw.
*
* @author Jørgen Austvik - Initial contribution
*/
@NonNullByDefault
public class DrawingSettings {

private static final Color COLOR_SIDE = Color.GRAY;
private static final Color COLOR_TEXT = Color.BLACK;

private final LayoutSettings layoutSettings;
private final int imageHeight;
private final ImagePoint2D min;
private final double rotationRadians;

public DrawingSettings(LayoutSettings layoutSettings, int imageHeight, ImagePoint2D min, double rotationRadians) {
this.imageHeight = imageHeight;
this.min = min;
this.rotationRadians = rotationRadians;
this.layoutSettings = layoutSettings;
}

public boolean shouldDrawLabels() {
return layoutSettings.shouldDrawLabels();
}

public boolean shouldDrawCorners() {
return layoutSettings.shouldDrawCorners();
}

public boolean shouldDrawOutline() {
return layoutSettings.shouldDrawOutline();
}

public boolean shouldFillWithColor() {
return layoutSettings.shouldFillWithColor();
}

public Color getOutlineColor() {
return COLOR_SIDE;
}

public Color getLabelColor() {
return COLOR_TEXT;
}

public ImagePoint2D generateImagePoint(Point2D point) {
return toPictureLayout(point, imageHeight, min, rotationRadians);
}

public List<ImagePoint2D> generateImagePoints(List<Point2D> points) {
return toPictureLayout(points, imageHeight, min, rotationRadians);
}

private static ImagePoint2D toPictureLayout(Point2D original, int imageHeight, ImagePoint2D min,
double rotationRadians) {
Point2D rotated = original.rotate(rotationRadians);
ImagePoint2D translated = new ImagePoint2D(
NanoleafBindingConstants.LAYOUT_BORDER_WIDTH + rotated.getX() - min.getX(),
imageHeight - NanoleafBindingConstants.LAYOUT_BORDER_WIDTH - rotated.getY() + min.getY());
return translated;
}

private static List<ImagePoint2D> toPictureLayout(List<Point2D> originals, int imageHeight, ImagePoint2D min,
double rotationRadians) {
List<ImagePoint2D> result = new ArrayList<>(originals.size());
for (Point2D original : originals) {
result.add(toPictureLayout(original, imageHeight, min, rotationRadians));
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit cd75cde

Please sign in to comment.