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