2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nanoleaf.internal.handler;
15 import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.Arrays;
19 import java.util.HashMap;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.client.api.ContentResponse;
28 import org.eclipse.jetty.client.api.Request;
29 import org.eclipse.jetty.client.util.StringContentProvider;
30 import org.eclipse.jetty.http.HttpMethod;
31 import org.openhab.binding.nanoleaf.internal.*;
32 import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
33 import org.openhab.binding.nanoleaf.internal.model.Effects;
34 import org.openhab.binding.nanoleaf.internal.model.Write;
35 import org.openhab.core.library.types.*;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.ThingStatusInfo;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.thing.binding.BridgeHandler;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import com.google.gson.Gson;
52 * The {@link NanoleafPanelHandler} is responsible for handling commands to the controller which
53 * affect an individual panels
55 * @author Martin Raepple - Initial contribution
56 * @author Stefan Höhn - Canvas Touch Support
59 public class NanoleafPanelHandler extends BaseThingHandler {
61 private static final PercentType MIN_PANEL_BRIGHTNESS = PercentType.ZERO;
62 private static final PercentType MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED;
64 private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);
66 private HttpClient httpClient;
67 // JSON parser for API responses
68 private final Gson gson = new Gson();
70 // holds current color data per panel
71 private Map<String, HSBType> panelInfo = new HashMap<>();
73 private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
74 private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
76 public NanoleafPanelHandler(Thing thing, HttpClient httpClient) {
78 this.httpClient = httpClient;
82 public void initialize() {
83 logger.debug("Initializing handler for panel {}", getThing().getUID());
84 updateStatus(ThingStatus.OFFLINE);
85 Bridge controller = getBridge();
86 if (controller == null) {
87 initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, ""));
88 } else if (ThingStatus.OFFLINE.equals(controller.getStatus())) {
89 initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
90 "@text/error.nanoleaf.panel.controllerOffline"));
92 initializePanel(controller.getStatusInfo());
97 public void bridgeStatusChanged(ThingStatusInfo controllerStatusInfo) {
98 logger.debug("Controller status changed to {} -- {}", controllerStatusInfo,
99 controllerStatusInfo.getDescription() + "/" + controllerStatusInfo.getStatus() + "/"
100 + controllerStatusInfo.hashCode());
101 if (controllerStatusInfo.getStatus().equals(ThingStatus.OFFLINE)) {
102 initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
103 "@text/error.nanoleaf.panel.controllerOffline"));
105 initializePanel(controllerStatusInfo);
110 public void handleCommand(ChannelUID channelUID, Command command) {
111 logger.debug("Received command {} for channel {}", command, channelUID);
113 switch (channelUID.getId()) {
114 case CHANNEL_PANEL_COLOR:
115 sendRenderedEffectCommand(command);
118 logger.warn("Channel with id {} not handled", channelUID.getId());
121 } catch (NanoleafUnauthorizedException nae) {
122 logger.warn("Authorization for command {} for channelUID {} failed: {}", command, channelUID,
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
125 "@text/error.nanoleaf.controller.invalidToken");
126 } catch (NanoleafException ne) {
127 logger.warn("Handling command {} for channelUID {} failed: {}", command, channelUID, ne.getMessage());
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
129 "@text/error.nanoleaf.controller.communication");
134 public void handleRemoval() {
135 logger.debug("Nanoleaf panel {} removed", getThing().getUID());
136 super.handleRemoval();
140 public void dispose() {
141 logger.debug("Disposing handler for Nanoleaf panel {}", getThing().getUID());
146 private void stopAllJobs() {
147 if (singleTapJob != null && !singleTapJob.isCancelled()) {
148 logger.debug("Stop single touch job");
149 singleTapJob.cancel(true);
150 this.singleTapJob = null;
152 if (doubleTapJob != null && !doubleTapJob.isCancelled()) {
153 logger.debug("Stop double touch job");
154 doubleTapJob.cancel(true);
155 this.doubleTapJob = null;
159 private void initializePanel(ThingStatusInfo panelStatus) {
160 updateStatus(panelStatus.getStatus(), panelStatus.getStatusDetail());
161 logger.debug("Panel {} status changed to {}-{}", this.getThing().getUID(), panelStatus.getStatus(),
162 panelStatus.getStatusDetail());
165 private void sendRenderedEffectCommand(Command command) throws NanoleafException {
166 logger.debug("Command Type: {}", command.getClass());
167 HSBType currentPanelColor = getPanelColor();
168 if (currentPanelColor != null)
169 logger.debug("currentPanelColor: {}", currentPanelColor.toString());
170 HSBType newPanelColor = new HSBType();
172 if (command instanceof HSBType) {
173 newPanelColor = (HSBType) command;
174 } else if (command instanceof OnOffType && (currentPanelColor != null)) {
175 if (OnOffType.ON.equals(command)) {
176 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
177 MAX_PANEL_BRIGHTNESS);
179 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
180 MIN_PANEL_BRIGHTNESS);
182 } else if (command instanceof PercentType && (currentPanelColor != null)) {
183 PercentType brightness = new PercentType(
184 Math.max(MIN_PANEL_BRIGHTNESS.intValue(), ((PercentType) command).intValue()));
185 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), brightness);
186 } else if (command instanceof IncreaseDecreaseType && (currentPanelColor != null)) {
187 int brightness = currentPanelColor.getBrightness().intValue();
188 if (command.equals(IncreaseDecreaseType.INCREASE)) {
189 brightness = Math.min(MAX_PANEL_BRIGHTNESS.intValue(), brightness + BRIGHTNESS_STEP_SIZE);
191 brightness = Math.max(MIN_PANEL_BRIGHTNESS.intValue(), brightness - BRIGHTNESS_STEP_SIZE);
193 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
194 new PercentType(brightness));
195 } else if (command instanceof RefreshType) {
196 logger.debug("Refresh command received");
199 logger.warn("Unhandled command type: {}", command.getClass().getName());
202 // store panel's new HSB value
203 logger.trace("Setting new color {}", newPanelColor);
204 panelInfo.put(getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(), newPanelColor);
206 PercentType[] rgbPercent = newPanelColor.toRGB();
207 logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]);
208 int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
209 .multiply(new BigDecimal(255)).intValue();
210 int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
211 .multiply(new BigDecimal(255)).intValue();
212 int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP)
213 .multiply(new BigDecimal(255)).intValue();
214 logger.trace("Setting new rgb {} {} {}", red, green, blue);
215 Bridge bridge = getBridge();
216 if (bridge != null) {
217 Effects effects = new Effects();
218 Write write = new Write();
219 write.setCommand("display");
220 write.setAnimType("static");
221 String panelID = this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString();
223 BridgeHandler handler = bridge.getHandler();
224 if (handler != null) {
225 NanoleafControllerConfig config = ((NanoleafControllerHandler) handler).getControllerConfig();
226 // Light Panels and Canvas use different stream commands
227 if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
228 || config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
229 logger.trace("Anim Data rgb {} {} {} {}", panelID, red, green, blue);
230 write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue));
232 // this is only used in special streaming situations with canvas which is not yet supported
233 int quotient = Integer.divideUnsigned(Integer.valueOf(panelID), 256);
234 int remainder = Integer.remainderUnsigned(Integer.valueOf(panelID), 256);
236 String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue));
238 write.setLoop(false);
239 effects.setWrite(write);
240 Request setNewRenderedEffectRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
242 String content = gson.toJson(effects);
243 logger.debug("sending effect command from panel {}: {}", getThing().getUID(), content);
244 setNewRenderedEffectRequest.content(new StringContentProvider(content), "application/json");
245 OpenAPIUtils.sendOpenAPIRequest(setNewRenderedEffectRequest);
247 logger.warn("Couldn't set rendering effect as Bridge-Handler {} is null", bridge.getUID());
252 public void updatePanelColorChannel() {
254 HSBType panelColor = getPanelColor();
255 logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
256 if (panelColor != null)
257 updateState(CHANNEL_PANEL_COLOR, panelColor);
261 * Apply the gesture to the panel
263 * @param gesture Only 0=single tap and 1=double tap are supported
265 public void updatePanelGesture(int gesture) {
268 updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.ON);
269 singleTapJob = scheduler.schedule(this::resetSingleTap, 1, TimeUnit.SECONDS);
270 logger.debug("Asserting single tap of panel {} to ON", getPanelID());
273 updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.ON);
274 doubleTapJob = scheduler.schedule(this::resetDoubleTap, 1, TimeUnit.SECONDS);
275 logger.debug("Asserting double tap of panel {} to ON", getPanelID());
280 private void resetSingleTap() {
281 updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.OFF);
282 logger.debug("Resetting single tap of panel {} to OFF", getPanelID());
285 private void resetDoubleTap() {
286 updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.OFF);
287 logger.debug("Resetting double tap of panel {} to OFF", getPanelID());
290 public String getPanelID() {
291 String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString();
295 private @Nullable HSBType getPanelColor() {
296 String panelID = getPanelID();
298 // get panel color data from controller
300 Effects effects = new Effects();
301 Write write = new Write();
302 write.setCommand("request");
303 write.setAnimName("*Static*");
304 effects.setWrite(write);
305 Bridge bridge = getBridge();
306 if (bridge != null) {
307 NanoleafControllerHandler handler = (NanoleafControllerHandler) bridge.getHandler();
308 if (handler != null) {
309 NanoleafControllerConfig config = handler.getControllerConfig();
310 logger.debug("Sending Request from Panel for getColor()");
311 Request setPanelUpdateRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
313 setPanelUpdateRequest.content(new StringContentProvider(gson.toJson(effects)), "application/json");
314 ContentResponse panelData = OpenAPIUtils.sendOpenAPIRequest(setPanelUpdateRequest);
317 parsePanelData(panelID, config, panelData);
320 } catch (NanoleafNotFoundException nfe) {
321 logger.warn("Panel data could not be retrieved as no data was returned (static type missing?) : {}",
323 } catch (NanoleafBadRequestException nfe) {
325 "Panel data could not be retrieved as request not expected(static type missing / dynamic type on) : {}",
327 } catch (NanoleafException nue) {
328 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
329 "@text/error.nanoleaf.panel.communication");
330 logger.warn("Panel data could not be retrieved: {}", nue.getMessage());
333 return panelInfo.get(panelID);
336 void parsePanelData(String panelID, NanoleafControllerConfig config, ContentResponse panelData) {
337 // panelData is in format (numPanels, (PanelId, 1, R, G, B, W, TransitionTime) * numPanel)
339 Write response = gson.fromJson(panelData.getContentAsString(), Write.class);
340 if (response != null) {
341 String[] tokenizedData = response.getAnimData().split(" ");
342 if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
343 || config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
344 // panelData is in format (numPanels (PanelId 1 R G B W TransitionTime) * numPanel)
345 String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 1, tokenizedData.length);
346 for (int i = 0; i < panelDataPoints.length; i++) {
348 String id = panelDataPoints[i];
349 if (id.equals(panelID)) {
350 // found panel data - store it
351 panelInfo.put(panelID,
352 HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 2]),
353 Integer.parseInt(panelDataPoints[i + 3]),
354 Integer.parseInt(panelDataPoints[i + 4])));
359 // panelData is in format (0 numPanels (quotient(panelID) remainder(panelID) R G B W 0
360 // quotient(TransitionTime) remainder(TransitionTime)) * numPanel)
361 String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
362 for (int i = 0; i < panelDataPoints.length; i++) {
364 String idQuotient = panelDataPoints[i];
365 String idRemainder = panelDataPoints[i + 1];
366 Integer idNum = Integer.valueOf(idQuotient) * 256 + Integer.valueOf(idRemainder);
367 if (String.valueOf(idNum).equals(panelID)) {
368 // found panel data - store it
369 panelInfo.put(panelID,
370 HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 3]),
371 Integer.parseInt(panelDataPoints[i + 4]),
372 Integer.parseInt(panelDataPoints[i + 5])));