Skip to content

Commit

Permalink
[nanoleaf] Reduce layout logs, Fix wrong type detection, Add discover…
Browse files Browse the repository at this point in the history
…y props (#7180)

- Reduce layout logging by manual triggering
- Add more test for inconsistent layout response from device
- Fix wrong type detection for canvas
- Fix rediscovery end handling (could have put controller offline)
- Add model/vendor properties automatically if things configured manually in a static things file
- Some code cleanup

Signed-off-by: Stefan Höhn <stefan@andreaundstefanhoehn.de>

Co-authored-by: Stefan Höhn <stefan@andreaundstefanhoehn.de>
  • Loading branch information
stefan-hoehn and stefan-hoehn committed Mar 15, 2020
1 parent d6da6e5 commit cc22605
Show file tree
Hide file tree
Showing 11 changed files with 111 additions and 36 deletions.
16 changes: 12 additions & 4 deletions bundles/org.openhab.binding.nanoleaf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ Troubleshooting: In seldom cases (in particular together with updating the bindi
**Knowing which panel has which id**

Unfortunately it is not easy to find out which panel gets which id while this is pretty important if you have lots of them and you want to assign rules to it.
Don't worry: the binding comes with some helpful support in the background (this works only well for the canvas device!)
Don't worry: the binding comes with some helpful support in the background the canvas type (this is only provided for the canvas device because triangles can have weird layouts that are hard to express in a log output)

- fire up your browser and open the openhab server on port 9001 which shows the logs.
- Set up a switch item with the channel panelLayout on the controller (see NanoRetrieveLayout below) and set the switch to true
- look out for something like "Panel layout and ids" in the logs. Below that you will see a panel layout similar to

Compare the following output with the right picture at the beginning of the article
Expand All @@ -75,8 +76,9 @@ Compare the following output with the right picture at the beginning of the arti
41451
```

```
Disclaimer: this works best with square devices and not necessarily well with triangles due to the more geometrically flexible layout.

## Thing Configuration

Expand Down Expand Up @@ -114,6 +116,7 @@ The controller bridge has the following channels:
| 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 |
| panelLayout | Switch | Set to true will log out panel layout (returns to off automatically | No |

A lightpanel thing has the following channels:

Expand Down Expand Up @@ -168,15 +171,17 @@ The following files provide a full example for a configuration (using a things f
### nanoleaf.things

```
Bridge nanoleaf:controller:MyLightPanels [ address="192.168.1.100", port=16021, authToken="AbcDefGhiJk879LmNopqRstUv1234WxyZ", refreshInterval=60 ] {
Bridge nanoleaf:controller:MyLightPanels @ "mylocation" [ address="192.168.1.100", port=16021, authToken="AbcDefGhiJk879LmNopqRstUv1234WxyZ", refreshInterval=60 ] {
Thing lightpanel 135 [ id=135 ]
Thing lightpanel 158 [ id=158 ]
}
```

If you define your device statically in the thing file, autodiscovery of the same thing is suppressed by using

* the [address="..." ] of the controller
* and the [id=123] of the lightpanel

in the bracket to identify the uniqueness of the discovered device. Therefore it is recommended to the give the controller a fixed ip address.

Note: To generate the `authToken`:
Expand Down Expand Up @@ -208,6 +213,8 @@ String NanoleafEffect "Effect" { channel="nanoleaf:controller:MyLightPanels:effe
Switch NanoleafRhythmState "Rhythm connected [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmState" }
Switch NanoleafRhythmActive "Rhythm active [MAP(nanoleaf.map):%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmActive" }
Number NanoleafRhythmSource "Rhythm source [%s]" { channel="nanoleaf:controller:MyLightPanels:rhythmMode" }
Switch NanoRetrieveLayout "Nano Layout" { channel="nanoleaf:controller:D81E7A7E424E:panelLayout" }
// note that the next to items use the exact same channel but the two different types Color and Dimmer to control different parameters
Color Panel1Color "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" }
Dimmer Panel1Brightness "Panel 1" { channel="nanoleaf:lightpanel:MyLightPanels:135:panelColor" }
Expand Down Expand Up @@ -236,6 +243,7 @@ sitemap nanoleaf label="Nanoleaf"
Text item=NanoleafRhythmState
Text item=NanoleafRhythmActive
Selection item=NanoleafRhythmSource mappings=[0="Microphone", 1="Aux"]
Switch item=NanoRetrieveLayout
}
Frame label="Panels" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class NanoleafBindingConstants {
public static final String CHANNEL_RHYTHM_STATE = "rhythmState";
public static final String CHANNEL_RHYTHM_ACTIVE = "rhythmActive";
public static final String CHANNEL_RHYTHM_MODE = "rhythmMode";
public static final String CHANNEL_PANEL_LAYOUT = "panelLayout";

// List of light panel channels
public static final String CHANNEL_PANEL_COLOR = "panelColor";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Exception if request to Nanoleaf OpenAPI has been interrupted which is normally intended
*
* @author Stefan Höhn - Initial contribution
*/
@NonNullByDefault
public class NanoleafInterruptedException extends NanoleafException {

private static final long serialVersionUID = -6941678941424234257L;

public NanoleafInterruptedException(String message, InterruptedException interruptedException) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class OpenAPIUtils {
private static final Pattern FIRMWARE_VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)");

public static Request requestBuilder(HttpClient httpClient, NanoleafControllerConfig controllerConfig,
String apiOperation, HttpMethod method) throws NanoleafException, NanoleafUnauthorizedException {
String apiOperation, HttpMethod method) throws NanoleafException {
URI requestURI = getUri(controllerConfig, apiOperation, null);
LOGGER.trace("RequestBuilder: Sending Request {}:{} {} ", requestURI.getHost(), requestURI.getPort(),
requestURI.getPath());
Expand Down Expand Up @@ -88,11 +88,11 @@ public static URI getUri(NanoleafControllerConfig controllerConfig, String apiOp
}

public static ContentResponse sendOpenAPIRequest(Request request)
throws NanoleafException, NanoleafUnauthorizedException {
throws NanoleafException {
try {
traceSendRequest(request);

ContentResponse openAPIResponse = request.send();
ContentResponse openAPIResponse;
openAPIResponse = request.send();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("API response from Nanoleaf controller: {}", openAPIResponse.getContentAsString());
}
Expand All @@ -114,14 +114,16 @@ public static ContentResponse sendOpenAPIRequest(Request request)
openAPIResponse.getStatus()));
}
}
} catch (ExecutionException | TimeoutException | InterruptedException clientException) {
} catch (ExecutionException | TimeoutException clientException) {
if (clientException.getCause() instanceof HttpResponseException
&& ((HttpResponseException) clientException.getCause()).getResponse()
.getStatus() == HttpStatus.UNAUTHORIZED_401) {
LOGGER.warn("OpenAPI request unauthorized. Invalid authorization token.");
throw new NanoleafUnauthorizedException("Invalid authorization token");
}
throw new NanoleafException("Failed to send OpenAPI request", clientException);
} catch ( InterruptedException interruptedException) {
throw new NanoleafInterruptedException("OpenAPI request has been interrupted", interruptedException);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* The {@link NanoleafMDNSDiscoveryParticipant} is responsible for discovering new Nanoleaf controllers (bridges).
*
* @author Martin Raepple - Initial contribution
* @author Stefan Höhn
* @author Stefan Höhn - further improvements for static defined things
* @see <a href="https://www.eclipse.org/smarthome/documentation/development/bindings/discovery-services.html">MSDN Discovery</a>
*/
@Component(immediate = true, configurationPid = "discovery.nanoleaf")
Expand Down Expand Up @@ -84,6 +84,7 @@ public String getServiceType() {
logger.warn("Nanoleaf controller firmware is too old. Must be {} or higher",
MODEL_ID_LIGHTPANELS.equals(modelId) ? API_MIN_FW_VER_LIGHTPANELS : API_MIN_FW_VER_CANVAS);
}

final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withThingType(getThingType(service))
.withProperties(properties).withLabel(service.getName()).withRepresentationProperty(CONFIG_ADDRESS)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@
import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
import org.openhab.binding.nanoleaf.internal.NanoleafException;
import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
import org.openhab.binding.nanoleaf.internal.*;
import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
import org.openhab.binding.nanoleaf.internal.model.*;
import org.slf4j.Logger;
Expand Down Expand Up @@ -131,24 +128,21 @@ public void initialize() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noIp");
stopAllJobs();
return;
} else if (!StringUtils.isEmpty(getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))
&& !OpenAPIUtils.checkRequiredFirmware(getThing().getProperties().get(Thing.PROPERTY_MODEL_ID),
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))) {
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))) {
logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), API_MIN_FW_VER_LIGHTPANELS);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.nanoleaf.controller.incompatibleFirmware");
stopAllJobs();
return;
} else if (StringUtils.isEmpty(getAuthToken())) {
logger.debug("No token found. Start pairing background job");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
startPairingJob();
stopUpdateJob();
stopPanelDiscoveryJob();
return;
} else {
logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
updateStatus(ThingStatus.ONLINE);
Expand Down Expand Up @@ -181,6 +175,7 @@ public void handleCommand(ChannelUID channelUID, Command command) {
case CHANNEL_COLOR:
case CHANNEL_COLOR_TEMPERATURE:
case CHANNEL_COLOR_TEMPERATURE_ABS:
case CHANNEL_PANEL_LAYOUT:
sendStateCommand(channelUID.getId(), command);
break;
case CHANNEL_EFFECT:
Expand Down Expand Up @@ -320,9 +315,9 @@ private synchronized void stopPanelDiscoveryJob() {

private synchronized void startTouchJob() {
NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
if (config.deviceType != DEVICE_TYPE_CANVAS) {
logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type {}",
this.getThing().getUID(), config.deviceType);
if (!config.deviceType.equals(DEVICE_TYPE_CANVAS)) {
logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
this.getThing().getUID(), config.deviceType, DEVICE_TYPE_CANVAS);
return;
} else
logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
Expand Down Expand Up @@ -392,7 +387,6 @@ private void runPairing() {
if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
authTokenResponse.getStatus());
return;
} else {
// get auth token from response
@Nullable
Expand Down Expand Up @@ -456,6 +450,8 @@ private void runPanelDiscovery() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"@text/error.nanoleaf.controller.noToken");
}
} catch (NanoleafInterruptedException nie) {
logger.info("Panel discovery has been stopped.");
} catch (NanoleafException ne) {
logger.warn("Failed to discover panels: ", ne);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
Expand Down Expand Up @@ -534,7 +530,7 @@ public void onComplete(@Nullable Result result) {

/**
* Interate over all gathered touch events and apply them to the panel they belong to
*
*
* @param touchEvents
*/
private void handleTouchEvents(TouchEvents touchEvents) {
Expand All @@ -557,11 +553,13 @@ private void handleTouchEvents(TouchEvents touchEvents) {
});
}

private void updateFromControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
private void updateFromControllerInfo() throws NanoleafException {
logger.debug("Update channels for controller {}", thing.getUID());
this.controllerInfo = receiveControllerInfo();
if (controllerInfo == null)
if (controllerInfo == null) {
logger.debug("No Controller Info has been provided");
return;
}
final State state = controllerInfo.getState();

OnOffType powerState = state.getOnOff();
Expand Down Expand Up @@ -610,6 +608,8 @@ private void updateFromControllerInfo() throws NanoleafException, NanoleafUnauth
Map<String, String> properties = editProperties();
properties.put(Thing.PROPERTY_SERIAL_NUMBER, controllerInfo.getSerialNo());
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, controllerInfo.getFirmwareVersion());
properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
updateProperties(properties);

Configuration config = editConfiguration();
Expand Down Expand Up @@ -639,11 +639,6 @@ private void updateFromControllerInfo() throws NanoleafException, NanoleafUnauth
panelHandler.updatePanelColorChannel();
}
});

@Nullable
Layout layout = controllerInfo.getPanelLayout().getLayout();
String layoutView = (layout != null) ? layout.getLayoutView() : "";
logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
}

private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
Expand Down Expand Up @@ -763,6 +758,13 @@ private void sendStateCommand(String channel, Command command) throws NanoleafEx
return;
}
break;
case CHANNEL_PANEL_LAYOUT:
@Nullable
Layout layout = controllerInfo.getPanelLayout().getLayout();
String layoutView = (layout != null) ? layout.getLayoutView() : "";
logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF);
break;
default:
logger.warn("Unhandled command type: {}", command.getClass().getName());
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ public String getLayoutView() {
int miny = Integer.MAX_VALUE;
int maxy = Integer.MIN_VALUE;

for (int index = 0; index < numPanels; index++) {
if (positionData != null) {
final int noofDefinedPanels = positionData.size();
for (int index = 0; index < noofDefinedPanels; index++) {
if (positionData != null ) {
@Nullable
PositionDatum panel = positionData.get(index);

Expand All @@ -97,11 +98,11 @@ public String getLayoutView() {
int shiftWidth = getSideLength() / 2;

int lineY = maxy;
Map<Integer, PositionDatum> map = new TreeMap<>();
Map<Integer, PositionDatum> map;

while (lineY >= miny) {
map = new TreeMap<>();
for (int index = 0; index < numPanels; index++) {
for (int index = 0; index < noofDefinedPanels; index++) {

if (positionData != null) {
@Nullable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
binding.nanoleaf.name = Nanoleaf Binding
binding.nanoleaf.description = Integrates the Nanoleaf Light Panels (v100320)
binding.nanoleaf.description = Integrates the Nanoleaf Light Panels (v150320)

# thing types
thing-type.nanoleaf.controller.name = Nanoleaf Controller
Expand Down Expand Up @@ -40,6 +40,8 @@ channel-type.nanoleaf.rhythmActive.label = Rhythm Active
channel-type.nanoleaf.rhythmActive.description = Activity state of the rhythm module
channel-type.nanoleaf.rhythmMode.label = Rhythm Mode
channel-type.nanoleaf.rhythmMode.description = Sound source for the rhythm module (microphone or aux cable)
channel-type.nanoleaf.panelLayout.label = Panel Layout
channel-type.nanoleaf.panelLayout.description = Creates a panel layout upon request
channel-type.nanoleaf.panelColor.label = Panel Color
channel-type.nanoleaf.panelColor.description = Color of the individual panel
channel-type.nanoleaf.singleTap.label = SingleTap
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
binding.nanoleaf.name = Nanoleaf Binding
binding.nanoleaf.description = Binding für die Integration des Nanoleaf Light Panels (v100320)
binding.nanoleaf.description = Binding für die Integration des Nanoleaf Light Panels (v150320)

# thing types
thing-type.nanoleaf.controller.name = Nanoleaf Controller
Expand Down Expand Up @@ -40,6 +40,8 @@ channel-type.nanoleaf.rhythmActive.label = Rhythm Aktiv
channel-type.nanoleaf.rhythmActive.description = Zeigt an ob das Mikrofon des Rhythm Modules ativ ist.
channel-type.nanoleaf.rhythmMode.label = Rhythm Modus
channel-type.nanoleaf.rhythmMode.description = Erlaubt den Wechsel zwischen eingebautem Mikrofon und AUX-Kabel.
channel-type.nanoleaf.panelLayout.label = PanelLayout
channel-type.nanoleaf.panelLayout.description = Erzeugt auf Anfrage ein Panel-Layout
channel-type.nanoleaf.panelColor.label = Paneelfarbe
channel-type.nanoleaf.panelColor.description = Farbe des einzelnen Paneels
channel-type.nanoleaf.singleTap.label = Einzel-Tap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<channel id="rhythmState" typeId="rhythmState" />
<channel id="rhythmActive" typeId="rhythmActive" />
<channel id="rhythmMode" typeId="rhythmMode" />
<channel id="panelLayout" typeId="panelLayout" />
</channels>

<properties>
Expand Down Expand Up @@ -141,4 +142,11 @@
<description>@text/channel-type.nanoleaf.doubleTap.description</description>
<state readOnly="false" />
</channel-type>

<channel-type id="panelLayout">
<item-type>Switch</item-type>
<label>@text/channel-type.nanoleaf.panelLayout.label</label>
<description>@text/channel-type.nanoleaf.panelLayout.description</description>
<state readOnly="false" />
</channel-type>
</thing:thing-descriptions>
Loading

0 comments on commit cc22605

Please sign in to comment.