]> git.basschouten.com Git - openhab-addons.git/blob
ed2b7fa11805f353547f92b388dfa16f8d564205
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.concurrent.ScheduledFuture;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.client.HttpClient;
24 import org.eclipse.jetty.client.api.Request;
25 import org.eclipse.jetty.client.util.StringContentProvider;
26 import org.eclipse.jetty.http.HttpMethod;
27 import org.openhab.binding.nanoleaf.internal.NanoleafException;
28 import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
29 import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
30 import org.openhab.binding.nanoleaf.internal.colors.NanoleafPanelColorChangeListener;
31 import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
32 import org.openhab.binding.nanoleaf.internal.model.Effects;
33 import org.openhab.binding.nanoleaf.internal.model.Write;
34 import org.openhab.core.io.net.http.HttpClientFactory;
35 import org.openhab.core.library.types.HSBType;
36 import org.openhab.core.library.types.IncreaseDecreaseType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PercentType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.CommonTriggerEvents;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.ThingStatusInfo;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.BridgeHandler;
48 import org.openhab.core.thing.binding.ThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import com.google.gson.Gson;
55
56 /**
57  * The {@link NanoleafPanelHandler} is responsible for handling commands to the controller which
58  * affect an individual panels
59  *
60  * @author Martin Raepple - Initial contribution
61  * @author Stefan Höhn - Canvas Touch Support
62  */
63 @NonNullByDefault
64 public class NanoleafPanelHandler extends BaseThingHandler implements NanoleafPanelColorChangeListener {
65
66     private static final PercentType MIN_PANEL_BRIGHTNESS = PercentType.ZERO;
67     private static final PercentType MAX_PANEL_BRIGHTNESS = PercentType.HUNDRED;
68
69     private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class);
70
71     private final HttpClient httpClient;
72     // JSON parser for API responses
73     private final Gson gson = new Gson();
74     private HSBType currentPanelColor = HSBType.BLACK;
75
76     private @NonNullByDefault({}) ScheduledFuture<?> singleTapJob;
77     private @NonNullByDefault({}) ScheduledFuture<?> doubleTapJob;
78
79     public NanoleafPanelHandler(Thing thing, HttpClientFactory httpClientFactory) {
80         super(thing);
81         this.httpClient = httpClientFactory.getCommonHttpClient();
82     }
83
84     @Override
85     public void initialize() {
86         logger.debug("Initializing handler for panel {}", getThing().getUID());
87         Bridge controller = getBridge();
88         if (controller == null) {
89             initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, ""));
90         } else if (ThingStatus.OFFLINE.equals(controller.getStatus())) {
91             initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
92                     "@text/error.nanoleaf.panel.controllerOffline"));
93         } else {
94             initializePanel(controller.getStatusInfo());
95         }
96     }
97
98     @Override
99     public void bridgeStatusChanged(ThingStatusInfo controllerStatusInfo) {
100         logger.debug("Controller status changed to {} -- {}", controllerStatusInfo,
101                 controllerStatusInfo.getDescription() + "/" + controllerStatusInfo.getStatus() + "/"
102                         + controllerStatusInfo.hashCode());
103         if (controllerStatusInfo.getStatus().equals(ThingStatus.OFFLINE)) {
104             initializePanel(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
105                     "@text/error.nanoleaf.panel.controllerOffline"));
106         } else {
107             initializePanel(controllerStatusInfo);
108         }
109     }
110
111     @Override
112     public void handleCommand(ChannelUID channelUID, Command command) {
113         logger.debug("Received command {} for channel {}", command, channelUID);
114         try {
115             switch (channelUID.getId()) {
116                 case CHANNEL_PANEL_COLOR:
117                     sendRenderedEffectCommand(command);
118                     break;
119                 default:
120                     logger.warn("Channel with id {} not handled", channelUID.getId());
121                     break;
122             }
123         } catch (NanoleafUnauthorizedException nae) {
124             logger.warn("Authorization for command {} for channelUID {} failed: {}", command, channelUID,
125                     nae.getMessage());
126             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
127                     "@text/error.nanoleaf.controller.invalidToken");
128         } catch (NanoleafException ne) {
129             logger.warn("Handling command {} for channelUID {} failed: {}", command, channelUID, ne.getMessage());
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
131                     "@text/error.nanoleaf.controller.communication");
132         }
133     }
134
135     @Override
136     public void handleRemoval() {
137         logger.debug("Nanoleaf panel {} removed", getThing().getUID());
138         Bridge bridge = getBridge();
139         if (bridge != null) {
140             ThingHandler handler = bridge.getHandler();
141             if (handler instanceof NanoleafControllerHandler) {
142                 ((NanoleafControllerHandler) handler).getColorInformation().unregisterChangeListener(getPanelID());
143             }
144         }
145
146         super.handleRemoval();
147     }
148
149     @Override
150     public void dispose() {
151         logger.debug("Disposing handler for Nanoleaf panel {}", getThing().getUID());
152         stopAllJobs();
153         super.dispose();
154     }
155
156     private void stopAllJobs() {
157         if (singleTapJob != null && !singleTapJob.isCancelled()) {
158             logger.debug("Stop single touch job");
159             singleTapJob.cancel(true);
160             this.singleTapJob = null;
161         }
162         if (doubleTapJob != null && !doubleTapJob.isCancelled()) {
163             logger.debug("Stop double touch job");
164             doubleTapJob.cancel(true);
165             this.doubleTapJob = null;
166         }
167     }
168
169     private void initializePanel(ThingStatusInfo panelStatus) {
170         updateStatus(panelStatus.getStatus(), panelStatus.getStatusDetail());
171         updateState(CHANNEL_PANEL_COLOR, currentPanelColor);
172         logger.debug("Panel {} status changed to {}-{}", this.getThing().getUID(), panelStatus.getStatus(),
173                 panelStatus.getStatusDetail());
174
175         Bridge bridge = getBridge();
176         if (bridge != null) {
177             ThingHandler handler = bridge.getHandler();
178             if (handler instanceof NanoleafControllerHandler) {
179                 ((NanoleafControllerHandler) handler).getColorInformation().registerChangeListener(getPanelID(), this);
180             }
181         }
182     }
183
184     private void sendRenderedEffectCommand(Command command) throws NanoleafException {
185         logger.debug("Command Type: {}", command.getClass());
186         logger.debug("currentPanelColor: {}", currentPanelColor);
187
188         HSBType newPanelColor = new HSBType();
189         if (command instanceof HSBType) {
190             newPanelColor = (HSBType) command;
191         } else if (command instanceof OnOffType) {
192             if (OnOffType.ON.equals(command)) {
193                 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
194                         MAX_PANEL_BRIGHTNESS);
195             } else {
196                 newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
197                         MIN_PANEL_BRIGHTNESS);
198             }
199         } else if (command instanceof PercentType) {
200             PercentType brightness = new PercentType(
201                     Math.max(MIN_PANEL_BRIGHTNESS.intValue(), ((PercentType) command).intValue()));
202             newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(), brightness);
203         } else if (command instanceof IncreaseDecreaseType) {
204             int brightness = currentPanelColor.getBrightness().intValue();
205             if (command.equals(IncreaseDecreaseType.INCREASE)) {
206                 brightness = Math.min(MAX_PANEL_BRIGHTNESS.intValue(), brightness + BRIGHTNESS_STEP_SIZE);
207             } else {
208                 brightness = Math.max(MIN_PANEL_BRIGHTNESS.intValue(), brightness - BRIGHTNESS_STEP_SIZE);
209             }
210             newPanelColor = new HSBType(currentPanelColor.getHue(), currentPanelColor.getSaturation(),
211                     new PercentType(brightness));
212         } else if (command instanceof RefreshType) {
213             logger.debug("Refresh command received");
214             return;
215         } else {
216             logger.warn("Unhandled command type: {}", command.getClass().getName());
217             return;
218         }
219         // store panel's new HSB value
220         logger.trace("Setting new color {} to panel {}", newPanelColor, getPanelID());
221         setPanelColor(newPanelColor);
222         // transform to RGB
223         PercentType[] rgbPercent = newPanelColor.toRGB();
224         logger.trace("Setting new rgbpercent {} {} {}", rgbPercent[0], rgbPercent[1], rgbPercent[2]);
225         int red = rgbPercent[0].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
226                 .multiply(new BigDecimal(255)).intValue();
227         int green = rgbPercent[1].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
228                 .multiply(new BigDecimal(255)).intValue();
229         int blue = rgbPercent[2].toBigDecimal().divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
230                 .multiply(new BigDecimal(255)).intValue();
231         logger.trace("Setting new rgb {} {} {}", red, green, blue);
232         Bridge bridge = getBridge();
233         if (bridge != null) {
234             Effects effects = new Effects();
235             Write write = new Write();
236             write.setCommand("display");
237             write.setAnimType("static");
238             Integer panelID = Integer.valueOf(this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString());
239             @Nullable
240             BridgeHandler handler = bridge.getHandler();
241             if (handler != null) {
242                 NanoleafControllerConfig config = ((NanoleafControllerHandler) handler).getControllerConfig();
243                 // Light Panels and Canvas use different stream commands
244                 if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
245                         || config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
246                     logger.trace("Anim Data rgb {} {} {} {}", panelID, red, green, blue);
247                     write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue));
248                 } else {
249                     // this is only used in special streaming situations with canvas which is not yet supported
250                     int quotient = Integer.divideUnsigned(panelID, 256);
251                     int remainder = Integer.remainderUnsigned(panelID, 256);
252                     write.setAnimData(
253                             String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue));
254                 }
255                 write.setLoop(false);
256                 effects.setWrite(write);
257                 Request setNewRenderedEffectRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT,
258                         HttpMethod.PUT);
259                 String content = gson.toJson(effects);
260                 logger.debug("sending effect command from panel {}: {}", getThing().getUID(), content);
261                 setNewRenderedEffectRequest.content(new StringContentProvider(content), "application/json");
262                 OpenAPIUtils.sendOpenAPIRequest(setNewRenderedEffectRequest);
263             } else {
264                 logger.warn("Couldn't set rendering effect as Bridge-Handler {} is null", bridge.getUID());
265             }
266         }
267     }
268
269     /**
270      * Apply the gesture to the panel
271      *
272      * @param gesture Only 0=single tap, 1=double tap and 6=long press are supported
273      */
274     public void updatePanelGesture(int gesture) {
275         switch (gesture) {
276             case 0:
277                 triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.SHORT_PRESSED);
278                 break;
279             case 1:
280                 triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.DOUBLE_PRESSED);
281                 break;
282             case 6:
283                 triggerChannel(CHANNEL_PANEL_TAP, CommonTriggerEvents.LONG_PRESSED);
284                 break;
285         }
286     }
287
288     public Integer getPanelID() {
289         Object panelId = getThing().getConfiguration().get(CONFIG_PANEL_ID);
290         if (panelId instanceof Integer) {
291             return (Integer) panelId;
292         } else if (panelId instanceof Number) {
293             return ((Number) panelId).intValue();
294         } else {
295             // Fall back to parsing string representation of panel if it is not returning an integer
296             String stringPanelId = panelId.toString();
297             Integer parsedPanelId = Integer.getInteger(stringPanelId);
298             if (parsedPanelId == null) {
299                 return 0;
300             } else {
301                 return parsedPanelId;
302             }
303         }
304     }
305
306     private void setPanelColor(HSBType color) {
307         Integer panelId = getPanelID();
308         Bridge bridge = getBridge();
309         if (bridge != null) {
310             ThingHandler handler = bridge.getHandler();
311             if (handler instanceof NanoleafControllerHandler) {
312                 ((NanoleafControllerHandler) handler).getColorInformation().setPanelColor(panelId, color);
313             } else {
314                 logger.debug("Couldn't find handler for panel {}", panelId);
315             }
316         } else {
317             logger.debug("Couldn't find bridge for panel {}", panelId);
318         }
319     }
320
321     @Override
322     public void onPanelChangedColor(HSBType newColor) {
323         if (logger.isTraceEnabled()) {
324             logger.trace("updatePanelColorChannel: panelColor: {} for panel {}", newColor, getPanelID());
325         }
326
327         currentPanelColor = newColor;
328         updateState(CHANNEL_PANEL_COLOR, newColor);
329     }
330 }