]> git.basschouten.com Git - openhab-addons.git/blob
7eb9d15509722239e44e4385d5231fe2c105ad45
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.nanoleaf.internal.handler;
14
15 import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.Arrays;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
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;
48
49 import com.google.gson.Gson;
50
51 /**
52  * The {@link NanoleafPanelHandler} is responsible for handling commands to the controller which
53  * affect an individual panels
54  *
55  * @author Martin Raepple - Initial contribution
56  * @author Stefan Höhn - Canvas Touch Support
57  */
58 @NonNullByDefault
59 public class NanoleafPanelHandler extends BaseThingHandler {
60
61     private static final PercentType MIN_PANEL_BRIGHTNESS = PercentType.ZERO;
62     private static final PercentType MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED;
63
64     private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);
65
66     private HttpClient httpClient;
67     // JSON parser for API responses
68     private final Gson gson = new Gson();
69
70     // holds current color data per panel
71     private Map<String, HSBType> panelInfo = new HashMap<>();
72
73     private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
74     private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
75
76     public NanoleafPanelHandler(Thing thing, HttpClient httpClient) {
77         super(thing);
78         this.httpClient = httpClient;
79     }
80
81     @Override
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"));
91         } else {
92             initializePanel(controller.getStatusInfo());
93         }
94     }
95
96     @Override
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"));
104         } else {
105             initializePanel(controllerStatusInfo);
106         }
107     }
108
109     @Override
110     public void handleCommand(ChannelUID channelUID, Command command) {
111         logger.debug("Received command {} for channel {}", command, channelUID);
112         try {
113             switch (channelUID.getId()) {
114                 case CHANNEL_PANEL_COLOR:
115                     sendRenderedEffectCommand(command);
116                     break;
117                 default:
118                     logger.warn("Channel with id {} not handled", channelUID.getId());
119                     break;
120             }
121         } catch (NanoleafUnauthorizedException nae) {
122             logger.warn("Authorization for command {} for channelUID {} failed: {}", command, channelUID,
123                     nae.getMessage());
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");
130         }
131     }
132
133     @Override
134     public void handleRemoval() {
135         logger.debug("Nanoleaf panel {} removed", getThing().getUID());
136         super.handleRemoval();
137     }
138
139     @Override
140     public void dispose() {
141         logger.debug("Disposing handler for Nanoleaf panel {}", getThing().getUID());
142         stopAllJobs();
143         super.dispose();
144     }
145
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;
151         }
152         if (doubleTapJob != null && !doubleTapJob.isCancelled()) {
153             logger.debug("Stop double touch job");
154             doubleTapJob.cancel(true);
155             this.doubleTapJob = null;
156         }
157     }
158
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());
163     }
164
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();
171
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);
178             } else {
179                 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
180                         MIN_PANEL_BRIGHTNESS);
181             }
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);
190             } else {
191                 brightness = Math.max(MIN_PANEL_BRIGHTNESS.intValue(), brightness - BRIGHTNESS_STEP_SIZE);
192             }
193             newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
194                     new PercentType(brightness));
195         } else if (command instanceof RefreshType) {
196             logger.debug("Refresh command received");
197             return;
198         } else {
199             logger.warn("Unhandled command type: {}", command.getClass().getName());
200             return;
201         }
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);
205         // transform to RGB
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();
222             @Nullable
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));
231                 } else {
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);
235                     write.setAnimData(
236                             String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue));
237                 }
238                 write.setLoop(false);
239                 effects.setWrite(write);
240                 Request setNewRenderedEffectRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
241                         HttpMethod.PUT);
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);
246             } else {
247                 logger.warn("Couldn't set rendering effect as Bridge-Handler {} is null", bridge.getUID());
248             }
249         }
250     }
251
252     public void updatePanelColorChannel() {
253         @Nullable
254         HSBType panelColor = getPanelColor();
255         logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
256         if (panelColor != null)
257             updateState(CHANNEL_PANEL_COLOR, panelColor);
258     }
259
260     /**
261      * Apply the gesture to the panel
262      *
263      * @param gesture Only 0=single tap and 1=double tap are supported
264      */
265     public void updatePanelGesture(int gesture) {
266         switch (gesture) {
267             case 0:
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());
271                 break;
272             case 1:
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());
276                 break;
277         }
278     }
279
280     private void resetSingleTap() {
281         updateState(CHANNEL_PANEL_SINGLE_TAP, OnOffType.OFF);
282         logger.debug("Resetting single tap of panel {} to OFF", getPanelID());
283     }
284
285     private void resetDoubleTap() {
286         updateState(CHANNEL_PANEL_DOUBLE_TAP, OnOffType.OFF);
287         logger.debug("Resetting double tap of panel {} to OFF", getPanelID());
288     }
289
290     public String getPanelID() {
291         String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString();
292         return panelID;
293     }
294
295     private @Nullable HSBType getPanelColor() {
296         String panelID = getPanelID();
297
298         // get panel color data from controller
299         try {
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,
312                             HttpMethod.PUT);
313                     setPanelUpdateRequest.content(new StringContentProvider(gson.toJson(effects)), "application/json");
314                     ContentResponse panelData = OpenAPIUtils.sendOpenAPIRequest(setPanelUpdateRequest);
315                     // parse panel data
316
317                     parsePanelData(panelID, config, panelData);
318                 }
319             }
320         } catch (NanoleafNotFoundException nfe) {
321             logger.warn("Panel data could not be retrieved as no data was returned (static type missing?) : {}",
322                     nfe.getMessage());
323         } catch (NanoleafBadRequestException nfe) {
324             logger.debug(
325                     "Panel data could not be retrieved as request not expected(static type missing / dynamic type on) : {}",
326                     nfe.getMessage());
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());
331         }
332
333         return panelInfo.get(panelID);
334     }
335
336     void parsePanelData(String panelID, NanoleafControllerConfig config, ContentResponse panelData) {
337         // panelData is in format (numPanels, (PanelId, 1, R, G, B, W, TransitionTime) * numPanel)
338         @Nullable
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++) {
347                     if (i % 7 == 0) {
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])));
355                         }
356                     }
357                 }
358             } else {
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++) {
363                     if (i % 8 == 0) {
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])));
373                         }
374                     }
375                 }
376             }
377         }
378     }
379 }