]> git.basschouten.com Git - openhab-addons.git/blob
995eb8a3479e327a292febff790e0adf1d6506bf
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.math.RoundingMode;
19 import java.util.Arrays;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.concurrent.ScheduledFuture;
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.NanoleafBadRequestException;
32 import org.openhab.binding.nanoleaf.internal.NanoleafException;
33 import org.openhab.binding.nanoleaf.internal.NanoleafNotFoundException;
34 import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
35 import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
36 import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
37 import org.openhab.binding.nanoleaf.internal.model.Effects;
38 import org.openhab.binding.nanoleaf.internal.model.Write;
39 import org.openhab.core.io.net.http.HttpClientFactory;
40 import org.openhab.core.library.types.HSBType;
41 import org.openhab.core.library.types.IncreaseDecreaseType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.PercentType;
44 import org.openhab.core.thing.Bridge;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.CommonTriggerEvents;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingStatusInfo;
51 import org.openhab.core.thing.binding.BaseThingHandler;
52 import org.openhab.core.thing.binding.BridgeHandler;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.RefreshType;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 import com.google.gson.Gson;
59
60 /**
61  * The {@link NanoleafPanelHandler} is responsible for handling commands to the controller which
62  * affect an individual panels
63  *
64  * @author Martin Raepple - Initial contribution
65  * @author Stefan Höhn - Canvas Touch Support
66  */
67 @NonNullByDefault
68 public class NanoleafPanelHandler extends BaseThingHandler {
69
70     private static final PercentType MIN_PANEL_BRIGHTNESS = PercentType.ZERO;
71     private static final PercentType MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED;
72
73     private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);
74
75     private final HttpClient httpClient;
76     // JSON parser for API responses
77     private final Gson gson = new Gson();
78
79     // holds current color data per panel
80     private final Map<String, HSBType> panelInfo = new HashMap<>();
81
82     private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
83     private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
84
85     public NanoleafPanelHandler(Thing thing, HttpClientFactory httpClientFactory) {
86         super(thing);
87         this.httpClient = httpClientFactory.getCommonHttpClient();
88     }
89
90     @Override
91     public void initialize() {
92         logger.debug("Initializing handler for panel {}", getThing().getUID());
93         Bridge controller = getBridge();
94         if (controller == null) {
95             initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, ""));
96         } else if (ThingStatus.OFFLINE.equals(controller.getStatus())) {
97             initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
98                     "@text/error.nanoleaf.panel.controllerOffline"));
99         } else {
100             initializePanel(controller.getStatusInfo());
101         }
102     }
103
104     @Override
105     public void bridgeStatusChanged(ThingStatusInfo controllerStatusInfo) {
106         logger.debug("Controller status changed to {} -- {}", controllerStatusInfo,
107                 controllerStatusInfo.getDescription() + "/" + controllerStatusInfo.getStatus() + "/"
108                         + controllerStatusInfo.hashCode());
109         if (controllerStatusInfo.getStatus().equals(ThingStatus.OFFLINE)) {
110             initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
111                     "@text/error.nanoleaf.panel.controllerOffline"));
112         } else {
113             initializePanel(controllerStatusInfo);
114         }
115     }
116
117     @Override
118     public void handleCommand(ChannelUID channelUID, Command command) {
119         logger.debug("Received command {} for channel {}", command, channelUID);
120         try {
121             switch (channelUID.getId()) {
122                 case CHANNEL_PANEL_COLOR:
123                     sendRenderedEffectCommand(command);
124                     break;
125                 default:
126                     logger.warn("Channel with id {} not handled", channelUID.getId());
127                     break;
128             }
129         } catch (NanoleafUnauthorizedException nae) {
130             logger.warn("Authorization for command {} for channelUID {} failed: {}", command, channelUID,
131                     nae.getMessage());
132             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
133                     "@text/error.nanoleaf.controller.invalidToken");
134         } catch (NanoleafException ne) {
135             logger.warn("Handling command {} for channelUID {} failed: {}", command, channelUID, ne.getMessage());
136             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
137                     "@text/error.nanoleaf.controller.communication");
138         }
139     }
140
141     @Override
142     public void handleRemoval() {
143         logger.debug("Nanoleaf panel {} removed", getThing().getUID());
144         super.handleRemoval();
145     }
146
147     @Override
148     public void dispose() {
149         logger.debug("Disposing handler for Nanoleaf panel {}", getThing().getUID());
150         stopAllJobs();
151         super.dispose();
152     }
153
154     private void stopAllJobs() {
155         if (singleTapJob != null && !singleTapJob.isCancelled()) {
156             logger.debug("Stop single touch job");
157             singleTapJob.cancel(true);
158             this.singleTapJob = null;
159         }
160         if (doubleTapJob != null && !doubleTapJob.isCancelled()) {
161             logger.debug("Stop double touch job");
162             doubleTapJob.cancel(true);
163             this.doubleTapJob = null;
164         }
165     }
166
167     private void initializePanel(ThingStatusInfo panelStatus) {
168         updateStatus(panelStatus.getStatus(), panelStatus.getStatusDetail());
169         logger.debug("Panel {} status changed to {}-{}", this.getThing().getUID(), panelStatus.getStatus(),
170                 panelStatus.getStatusDetail());
171     }
172
173     private void sendRenderedEffectCommand(Command command) throws NanoleafException {
174         logger.debug("Command Type: {}", command.getClass());
175         HSBType currentPanelColor = getPanelColor();
176         if (currentPanelColor != null) {
177             logger.debug("currentPanelColor: {}", currentPanelColor.toString());
178         }
179         HSBType newPanelColor = new HSBType();
180
181         if (command instanceof HSBType) {
182             newPanelColor = (HSBType) command;
183         } else if (command instanceof OnOffType && (currentPanelColor != null)) {
184             if (OnOffType.ON.equals(command)) {
185                 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
186                         MAX_PANEL_BRIGHTNESS);
187             } else {
188                 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
189                         MIN_PANEL_BRIGHTNESS);
190             }
191         } else if (command instanceof PercentType && (currentPanelColor != null)) {
192             PercentType brightness = new PercentType(
193                     Math.max(MIN_PANEL_BRIGHTNESS.intValue(), ((PercentType) command).intValue()));
194             newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), brightness);
195         } else if (command instanceof IncreaseDecreaseType && (currentPanelColor != null)) {
196             int brightness = currentPanelColor.getBrightness().intValue();
197             if (command.equals(IncreaseDecreaseType.INCREASE)) {
198                 brightness = Math.min(MAX_PANEL_BRIGHTNESS.intValue(), brightness + BRIGHTNESS_STEP_SIZE);
199             } else {
200                 brightness = Math.max(MIN_PANEL_BRIGHTNESS.intValue(), brightness - BRIGHTNESS_STEP_SIZE);
201             }
202             newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
203                     new PercentType(brightness));
204         } else if (command instanceof RefreshType) {
205             logger.debug("Refresh command received");
206             return;
207         } else {
208             logger.warn("Unhandled command type: {}", command.getClass().getName());
209             return;
210         }
211         // store panel's new HSB value
212         logger.trace("Setting new color {}", newPanelColor);
213         panelInfo.put(getThing().getConfiguration().get(CONFIG_PANEL_ID).toString(), newPanelColor);
214         // transform to RGB
215         PercentType[] rgbPercent = newPanelColor.toRGB();
216         logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]);
217         int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
218                 .multiply(new BigDecimal(255)).intValue();
219         int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
220                 .multiply(new BigDecimal(255)).intValue();
221         int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
222                 .multiply(new BigDecimal(255)).intValue();
223         logger.trace("Setting new rgb {} {} {}", red, green, blue);
224         Bridge bridge = getBridge();
225         if (bridge != null) {
226             Effects effects = new Effects();
227             Write write = new Write();
228             write.setCommand("display");
229             write.setAnimType("static");
230             Integer panelID = Integer.valueOf(this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString());
231             @Nullable
232             BridgeHandler handler = bridge.getHandler();
233             if (handler != null) {
234                 NanoleafControllerConfig config = ((NanoleafControllerHandler) handler).getControllerConfig();
235                 // Light Panels and Canvas use different stream commands
236                 if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
237                         || config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
238                     logger.trace("Anim Data rgb {} {} {} {}", panelID, red, green, blue);
239                     write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue));
240                 } else {
241                     // this is only used in special streaming situations with canvas which is not yet supported
242                     int quotient = Integer.divideUnsigned(panelID, 256);
243                     int remainder = Integer.remainderUnsigned(panelID, 256);
244                     write.setAnimData(
245                             String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue));
246                 }
247                 write.setLoop(false);
248                 effects.setWrite(write);
249                 Request setNewRenderedEffectRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
250                         HttpMethod.PUT);
251                 String content = gson.toJson(effects);
252                 logger.debug("sending effect command from panel {}: {}", getThing().getUID(), content);
253                 setNewRenderedEffectRequest.content(new StringContentProvider(content), "application/json");
254                 OpenAPIUtils.sendOpenAPIRequest(setNewRenderedEffectRequest);
255             } else {
256                 logger.warn("Couldn't set rendering effect as Bridge-Handler {} is null", bridge.getUID());
257             }
258         }
259     }
260
261     public void updatePanelColorChannel() {
262         @Nullable
263         HSBType panelColor = getPanelColor();
264         logger.trace("updatePanelColorChannel: panelColor: {}", panelColor);
265         if (panelColor != null) {
266             updateState(CHANNEL_PANEL_COLOR, panelColor);
267         }
268     }
269
270     /**
271      * Apply the gesture to the panel
272      *
273      * @param gesture Only 0=single tap and 1=double tap are supported
274      */
275     public void updatePanelGesture(int gesture) {
276         switch (gesture) {
277             case 0:
278                 triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.SHORT_PRESSED);
279                 break;
280             case 1:
281                 triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.DOUBLE_PRESSED);
282                 break;
283         }
284     }
285
286     public String getPanelID() {
287         String panelID = getThing().getConfiguration().get(CONFIG_PANEL_ID).toString();
288         return panelID;
289     }
290
291     public @Nullable HSBType getColor() {
292         String panelID = getPanelID();
293         return panelInfo.get(panelID);
294     }
295
296     private @Nullable HSBType getPanelColor() {
297         String panelID = getPanelID();
298
299         // get panel color data from controller
300         try {
301             Effects effects = new Effects();
302             Write write = new Write();
303             write.setCommand("request");
304             write.setAnimName("*Static*");
305             effects.setWrite(write);
306             Bridge bridge = getBridge();
307             if (bridge != null) {
308                 NanoleafControllerHandler handler = (NanoleafControllerHandler) bridge.getHandler();
309                 if (handler != null) {
310                     NanoleafControllerConfig config = handler.getControllerConfig();
311                     logger.debug("Sending Request from Panel for getColor()");
312                     Request setPanelUpdateRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
313                             HttpMethod.PUT);
314                     setPanelUpdateRequest.content(new StringContentProvider(gson.toJson(effects)), "application/json");
315                     ContentResponse panelData = OpenAPIUtils.sendOpenAPIRequest(setPanelUpdateRequest);
316                     // parse panel data
317
318                     parsePanelData(panelID, config, panelData);
319                 }
320             }
321         } catch (NanoleafNotFoundException nfe) {
322             logger.debug("Panel data could not be retrieved as no data was returned (static type missing?) : {}",
323                     nfe.getMessage());
324         } catch (NanoleafBadRequestException nfe) {
325             logger.debug(
326                     "Panel data could not be retrieved as request not expected(static type missing / dynamic type on) : {}",
327                     nfe.getMessage());
328         } catch (NanoleafException nue) {
329             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
330                     "@text/error.nanoleaf.panel.communication");
331             logger.debug("Panel data could not be retrieved: {}", nue.getMessage());
332         }
333
334         return panelInfo.get(panelID);
335     }
336
337     void parsePanelData(String panelID, NanoleafControllerConfig config, ContentResponse panelData) {
338         // panelData is in format (numPanels, (PanelId, 1, R, G, B, W, TransitionTime) * numPanel)
339         @Nullable
340         Write response = gson.fromJson(panelData.getContentAsString(), Write.class);
341         if (response != null) {
342             String[] tokenizedData = response.getAnimData().split(" ");
343             if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
344                     || config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
345                 // panelData is in format (numPanels (PanelId 1 R G B W TransitionTime) * numPanel)
346                 String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 1, tokenizedData.length);
347                 for (int i = 0; i < panelDataPoints.length; i++) {
348                     if (i % 7 == 0) {
349                         String id = panelDataPoints[i];
350                         if (id.equals(panelID)) {
351                             // found panel data - store it
352                             panelInfo.put(panelID,
353                                     HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 2]),
354                                             Integer.parseInt(panelDataPoints[i + 3]),
355                                             Integer.parseInt(panelDataPoints[i + 4])));
356                         }
357                     }
358                 }
359             } else {
360                 // panelData is in format (0 numPanels (quotient(panelID) remainder(panelID) R G B W 0
361                 // quotient(TransitionTime) remainder(TransitionTime)) * numPanel)
362                 String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
363                 for (int i = 0; i < panelDataPoints.length; i++) {
364                     if (i % 8 == 0) {
365                         Integer idQuotient = Integer.valueOf(panelDataPoints[i]);
366                         Integer idRemainder = Integer.valueOf(panelDataPoints[i + 1]);
367                         Integer idNum = idQuotient * 256 + idRemainder;
368                         if (String.valueOf(idNum).equals(panelID)) {
369                             // found panel data - store it
370                             panelInfo.put(panelID,
371                                     HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 3]),
372                                             Integer.parseInt(panelDataPoints[i + 4]),
373                                             Integer.parseInt(panelDataPoints[i + 5])));
374                         }
375                     }
376                 }
377             }
378         }
379     }
380 }