]> git.basschouten.com Git - openhab-addons.git/blob
76d27617c9e31c5efa9417d6255ce7fc190fc28b
[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.net.URI;
18 import java.nio.ByteBuffer;
19 import java.nio.charset.StandardCharsets;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Scanner;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
28
29 import org.apache.commons.lang.StringUtils;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.client.api.Request;
35 import org.eclipse.jetty.client.api.Response;
36 import org.eclipse.jetty.client.api.Result;
37 import org.eclipse.jetty.client.util.StringContentProvider;
38 import org.eclipse.jetty.http.HttpMethod;
39 import org.eclipse.jetty.http.HttpStatus;
40 import org.openhab.binding.nanoleaf.internal.*;
41 import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
42 import org.openhab.binding.nanoleaf.internal.model.*;
43 import org.openhab.core.config.core.Configuration;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.HSBType;
46 import org.openhab.core.library.types.IncreaseDecreaseType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.PercentType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.BaseBridgeHandler;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 import com.google.gson.Gson;
62 import com.google.gson.JsonSyntaxException;
63
64 /**
65  * The {@link NanoleafControllerHandler} is responsible for handling commands to the controller which
66  * affect all panels connected to it (e.g. selected effect)
67  *
68  * @author Martin Raepple - Initial contribution
69  * @author Stefan Höhn - Canvas Touch Support
70  */
71 @NonNullByDefault
72 public class NanoleafControllerHandler extends BaseBridgeHandler {
73
74     // Pairing interval in seconds
75     private static final int PAIRING_INTERVAL = 25;
76
77     // Panel discovery interval in seconds
78     private static final int PANEL_DISCOVERY_INTERVAL = 30;
79
80     private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
81     private HttpClient httpClient;
82     private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<>();
83
84     // Pairing, update and panel discovery jobs and touch event job
85     private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
86     private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
87     private @NonNullByDefault({}) ScheduledFuture<?> panelDiscoveryJob;
88     private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
89
90     // JSON parser for API responses
91     private final Gson gson = new Gson();
92
93     // Controller configuration settings and channel values
94     private @Nullable String address;
95     private int port;
96     private int refreshIntervall;
97     private @Nullable String authToken;
98     private @Nullable String deviceType;
99     private @NonNullByDefault({}) ControllerInfo controllerInfo;
100
101     public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) {
102         super(bridge);
103         this.httpClient = httpClient;
104     }
105
106     @Override
107     public void initialize() {
108         logger.debug("Initializing the controller (bridge)");
109         updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED);
110         NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
111         setAddress(config.address);
112         setPort(config.port);
113         setRefreshIntervall(config.refreshInterval);
114         setAuthToken(config.authToken);
115
116         @Nullable
117         String property = getThing().getProperties().get(Thing.PROPERTY_MODEL_ID);
118         if (MODEL_ID_CANVAS.equals(property)) {
119             config.deviceType = DEVICE_TYPE_CANVAS;
120         } else {
121             config.deviceType = DEVICE_TYPE_LIGHTPANELS;
122         }
123         setDeviceType(config.deviceType);
124
125         try {
126             if (StringUtils.isEmpty(getAddress()) || StringUtils.isEmpty(String.valueOf(getPort()))) {
127                 logger.warn("No IP address and port configured for the Nanoleaf controller");
128                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
129                         "@text/error.nanoleaf.controller.noIp");
130                 stopAllJobs();
131             } else if (!StringUtils.isEmpty(getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))
132                     && !OpenAPIUtils.checkRequiredFirmware(getThing().getProperties().get(Thing.PROPERTY_MODEL_ID),
133                             getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION))) {
134                 logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
135                         getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION), API_MIN_FW_VER_LIGHTPANELS);
136                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
137                         "@text/error.nanoleaf.controller.incompatibleFirmware");
138                 stopAllJobs();
139             } else if (StringUtils.isEmpty(getAuthToken())) {
140                 logger.debug("No token found. Start pairing background job");
141                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
142                         "@text/error.nanoleaf.controller.noToken");
143                 startPairingJob();
144                 stopUpdateJob();
145                 stopPanelDiscoveryJob();
146             } else {
147                 logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
148                 updateStatus(ThingStatus.ONLINE);
149                 stopPairingJob();
150                 startUpdateJob();
151                 startPanelDiscoveryJob();
152                 startTouchJob();
153             }
154         } catch (IllegalArgumentException iae) {
155             logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
156                     getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION));
157             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
158                     "@text/error.nanoleaf.controller.incompatibleFirmware");
159         }
160     }
161
162     @Override
163     public void handleCommand(ChannelUID channelUID, Command command) {
164         logger.debug("Received command {} for channel {}", command, channelUID);
165         if (!ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) {
166             logger.debug("Cannot handle command. Bridge is not online.");
167             return;
168         }
169         try {
170             if (command instanceof RefreshType) {
171                 updateFromControllerInfo();
172             } else {
173                 switch (channelUID.getId()) {
174                     case CHANNEL_POWER:
175                     case CHANNEL_COLOR:
176                     case CHANNEL_COLOR_TEMPERATURE:
177                     case CHANNEL_COLOR_TEMPERATURE_ABS:
178                     case CHANNEL_PANEL_LAYOUT:
179                         sendStateCommand(channelUID.getId(), command);
180                         break;
181                     case CHANNEL_EFFECT:
182                         sendEffectCommand(command);
183                         break;
184                     case CHANNEL_RHYTHM_MODE:
185                         sendRhythmCommand(command);
186                         break;
187                     default:
188                         logger.warn("Channel with id {} not handled", channelUID.getId());
189                         break;
190                 }
191             }
192         } catch (NanoleafUnauthorizedException nae) {
193             logger.warn("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
194                     nae.getMessage());
195             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
196                     "@text/error.nanoleaf.controller.invalidToken");
197         } catch (NanoleafException ne) {
198             logger.warn("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
199             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
200                     "@text/error.nanoleaf.controller.communication");
201         }
202     }
203
204     @Override
205     public void handleRemoval() {
206         // delete token for openHAB
207         ContentResponse deleteTokenResponse;
208         try {
209             Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER,
210                     HttpMethod.DELETE);
211             deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
212             if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
213                 logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus());
214                 return;
215             }
216             logger.debug("Successfully deleted token for openHAB from controller");
217         } catch (NanoleafUnauthorizedException e) {
218             logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
219         } catch (NanoleafException ne) {
220             logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
221         }
222         stopAllJobs();
223         super.handleRemoval();
224         logger.debug("Nanoleaf controller removed");
225     }
226
227     @Override
228     public void dispose() {
229         stopAllJobs();
230         super.dispose();
231         logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
232     }
233
234     public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
235         logger.debug("Register new listener for controller {}", getThing().getUID());
236         boolean result = controllerListeners.add(controllerListener);
237         if (result) {
238             startPanelDiscoveryJob();
239         }
240         return result;
241     }
242
243     public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
244         logger.debug("Unregister listener for controller {}", getThing().getUID());
245         boolean result = controllerListeners.remove(controllerListener);
246         if (result) {
247             stopPanelDiscoveryJob();
248         }
249         return result;
250     }
251
252     public NanoleafControllerConfig getControllerConfig() {
253         NanoleafControllerConfig config = new NanoleafControllerConfig();
254         config.address = this.getAddress();
255         config.port = this.getPort();
256         config.refreshInterval = this.getRefreshIntervall();
257         config.authToken = this.getAuthToken();
258         config.deviceType = this.getDeviceType();
259         return config;
260     }
261
262     public synchronized void startPairingJob() {
263         if (pairingJob == null || pairingJob.isCancelled()) {
264             logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
265             pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0, PAIRING_INTERVAL, TimeUnit.SECONDS);
266         }
267     }
268
269     private synchronized void stopPairingJob() {
270         if (pairingJob != null && !pairingJob.isCancelled()) {
271             logger.debug("Stop pairing job");
272             pairingJob.cancel(true);
273             this.pairingJob = null;
274         }
275     }
276
277     private synchronized void startUpdateJob() {
278         if (StringUtils.isNotEmpty(getAuthToken())) {
279             if (updateJob == null || updateJob.isCancelled()) {
280                 logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall());
281                 updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(),
282                         TimeUnit.SECONDS);
283             }
284         } else {
285             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
286                     "@text/error.nanoleaf.controller.noToken");
287         }
288     }
289
290     private synchronized void stopUpdateJob() {
291         if (updateJob != null && !updateJob.isCancelled()) {
292             logger.debug("Stop status job");
293             updateJob.cancel(true);
294             this.updateJob = null;
295         }
296     }
297
298     public synchronized void startPanelDiscoveryJob() {
299         logger.debug("Starting panel discovery job. Has Controller-Listeners: {} panelDiscoveryJob: {}",
300                 !controllerListeners.isEmpty(), panelDiscoveryJob);
301         if (!controllerListeners.isEmpty() && (panelDiscoveryJob == null || panelDiscoveryJob.isCancelled())) {
302             logger.debug("Start panel discovery job, interval={} sec", PANEL_DISCOVERY_INTERVAL);
303             panelDiscoveryJob = scheduler.scheduleWithFixedDelay(this::runPanelDiscovery, 0, PANEL_DISCOVERY_INTERVAL,
304                     TimeUnit.SECONDS);
305         }
306     }
307
308     private synchronized void stopPanelDiscoveryJob() {
309         if (controllerListeners.isEmpty() && panelDiscoveryJob != null && !panelDiscoveryJob.isCancelled()) {
310             logger.debug("Stop panel discovery job");
311             panelDiscoveryJob.cancel(true);
312             this.panelDiscoveryJob = null;
313         }
314     }
315
316     private synchronized void startTouchJob() {
317         NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
318         if (!config.deviceType.equals(DEVICE_TYPE_CANVAS)) {
319             logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
320                     this.getThing().getUID(), config.deviceType, DEVICE_TYPE_CANVAS);
321             return;
322         } else
323             logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
324
325         if (StringUtils.isNotEmpty(getAuthToken())) {
326             if (touchJob == null || touchJob.isCancelled()) {
327                 logger.debug("Starting Touchjob now");
328                 touchJob = scheduler.schedule(this::runTouchDetection, 0, TimeUnit.SECONDS);
329             }
330         } else {
331             logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID());
332         }
333     }
334
335     private synchronized void stopTouchJob() {
336         if (touchJob != null && !touchJob.isCancelled()) {
337             logger.debug("Stop touch job");
338             touchJob.cancel(true);
339             this.touchJob = null;
340         }
341     }
342
343     private void runUpdate() {
344         logger.debug("Run update job");
345         try {
346             updateFromControllerInfo();
347             startTouchJob(); // if device type has changed, start touch detection.
348             // controller might have been offline, e.g. for firmware update. In this case, return to online state
349             if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
350                 logger.debug("Controller {} is back online", thing.getUID());
351                 updateStatus(ThingStatus.ONLINE);
352             }
353         } catch (NanoleafUnauthorizedException nae) {
354             logger.warn("Status update unauthorized: {}", nae.getMessage());
355             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
356                     "@text/error.nanoleaf.controller.invalidToken");
357             if (StringUtils.isEmpty(getAuthToken())) {
358                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
359                         "@text/error.nanoleaf.controller.noToken");
360             }
361         } catch (NanoleafException ne) {
362             logger.warn("Status update failed: {}", ne.getMessage());
363             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
364                     "@text/error.nanoleaf.controller.communication");
365         } catch (RuntimeException e) {
366             logger.warn("Update job failed", e);
367             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
368         }
369     }
370
371     private void runPairing() {
372         logger.debug("Run pairing job");
373         try {
374             if (StringUtils.isNotEmpty(getAuthToken())) {
375                 if (pairingJob != null) {
376                     pairingJob.cancel(false);
377                 }
378                 logger.debug("Authentication token found. Canceling pairing job");
379                 return;
380             }
381             ContentResponse authTokenResponse = OpenAPIUtils
382                     .requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST).send();
383             if (logger.isTraceEnabled()) {
384                 logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
385             }
386
387             if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
388                 logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
389                         authTokenResponse.getStatus());
390             } else {
391                 // get auth token from response
392                 @Nullable
393                 AuthToken authToken = gson.fromJson(authTokenResponse.getContentAsString(), AuthToken.class);
394
395                 if (StringUtils.isNotEmpty(authToken.getAuthToken())) {
396                     logger.debug("Pairing succeeded.");
397
398                     // Update and save the auth token in the thing configuration
399                     Configuration config = editConfiguration();
400                     config.put(NanoleafControllerConfig.AUTH_TOKEN, authToken.getAuthToken());
401                     updateConfiguration(config);
402
403                     updateStatus(ThingStatus.ONLINE);
404                     // Update local field
405                     setAuthToken(authToken.getAuthToken());
406
407                     stopPairingJob();
408                     startUpdateJob();
409                     startPanelDiscoveryJob();
410                     startTouchJob();
411                 } else {
412                     logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
413                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
414                             "@text/error.nanoleaf.controller.pairingFailed");
415                     throw new NanoleafException(authTokenResponse.getContentAsString());
416                 }
417             }
418         } catch (JsonSyntaxException e) {
419             logger.warn("Received invalid data", e);
420             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
421                     "@text/error.nanoleaf.controller.invalidData");
422         } catch (NanoleafException e) {
423             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
424                     "@text/error.nanoleaf.controller.noTokenReceived");
425         } catch (InterruptedException | ExecutionException | TimeoutException e) {
426             logger.warn("Cannot send authorization request to controller: ", e);
427             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
428                     "@text/error.nanoleaf.controller.authRequest");
429         } catch (RuntimeException e) {
430             logger.warn("Pairing job failed", e);
431             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
432         } catch (Exception e) {
433             logger.warn("Cannot start http client", e);
434             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
435                     "@text/error.nanoleaf.controller.noClient");
436         }
437     }
438
439     private void runPanelDiscovery() {
440         logger.debug("Run panel discovery job");
441         // Trigger a new discovery of connected panels
442         for (NanoleafControllerListener controllerListener : controllerListeners) {
443             try {
444                 controllerListener.onControllerInfoFetched(getThing().getUID(), receiveControllerInfo());
445             } catch (NanoleafUnauthorizedException nue) {
446                 logger.warn("Panel discovery unauthorized: {}", nue.getMessage());
447                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
448                         "@text/error.nanoleaf.controller.invalidToken");
449                 if (StringUtils.isEmpty(getAuthToken())) {
450                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
451                             "@text/error.nanoleaf.controller.noToken");
452                 }
453             } catch (NanoleafInterruptedException nie) {
454                 logger.info("Panel discovery has been stopped.");
455             } catch (NanoleafException ne) {
456                 logger.warn("Failed to discover panels: ", ne);
457                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
458                         "@text/error.nanoleaf.controller.communication");
459             } catch (RuntimeException e) {
460                 logger.warn("Panel discovery job failed", e);
461                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
462             }
463         }
464     }
465
466     /**
467      * This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
468      */
469     private static boolean touchJobRunning = false;
470
471     private void runTouchDetection() {
472         if (touchJobRunning) {
473             logger.debug("touch job already running. quitting.");
474             return;
475         }
476         try {
477             touchJobRunning = true;
478             URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
479             logger.debug("touch job registered on: {}", eventUri.toString());
480             httpClient.newRequest(eventUri).send(new Response.Listener.Adapter() // request runs forever
481             {
482                 @Override
483                 public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
484                     String s = StandardCharsets.UTF_8.decode(content).toString();
485                     logger.trace("content {}", s);
486
487                     Scanner eventContent = new Scanner(s);
488                     while (eventContent.hasNextLine()) {
489                         String line = eventContent.nextLine().trim();
490                         // we don't expect anything than content id:4, so we do not check that but only care about the
491                         // data part
492                         if (line.startsWith("data:")) {
493                             String json = line.substring(5).trim(); // supposed to be JSON
494                             try {
495                                 @Nullable
496                                 TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
497                                 handleTouchEvents(touchEvents);
498                             } catch (JsonSyntaxException jse) {
499                                 logger.error("couldn't parse touch event json {}", json);
500                             }
501                         }
502                     }
503                     eventContent.close();
504                     logger.debug("leaving touch onContent");
505                     super.onContent(response, content);
506                 }
507
508                 @Override
509                 public void onSuccess(@Nullable Response response) {
510                     logger.trace("touch event SUCCESS: {}", response);
511                 }
512
513                 @Override
514                 public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
515                     logger.trace("touch event FAILURE: {}", response);
516                 }
517
518                 @Override
519                 public void onComplete(@Nullable Result result) {
520                     logger.trace("touch event COMPLETE: {}", result);
521                 }
522             });
523         } catch (RuntimeException | NanoleafException e) {
524             logger.warn("setting up TouchDetection failed", e);
525         } finally {
526             touchJobRunning = false;
527         }
528         logger.debug("leaving run touch detection");
529     }
530
531     /**
532      * Interate over all gathered touch events and apply them to the panel they belong to
533      *
534      * @param touchEvents
535      */
536     private void handleTouchEvents(TouchEvents touchEvents) {
537         touchEvents.getEvents().forEach(event -> {
538             logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
539
540             // Iterate over all child things = all panels of that controller
541             this.getThing().getThings().forEach(child -> {
542                 NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
543                 if (panelHandler != null) {
544                     logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
545                             event.getPanelId());
546                     if (panelHandler.getPanelID().equals(event.getPanelId())) {
547                         logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
548                                 event.getGesture());
549                         panelHandler.updatePanelGesture(event.getGesture());
550                     }
551                 }
552             });
553         });
554     }
555
556     private void updateFromControllerInfo() throws NanoleafException {
557         logger.debug("Update channels for controller {}", thing.getUID());
558         this.controllerInfo = receiveControllerInfo();
559         if (controllerInfo == null) {
560             logger.debug("No Controller Info has been provided");
561             return;
562         }
563         final State state = controllerInfo.getState();
564
565         OnOffType powerState = state.getOnOff();
566         updateState(CHANNEL_POWER, powerState);
567
568         @Nullable
569         Ct colorTemperature = state.getColorTemperature();
570
571         float colorTempPercent = 0f;
572         if (colorTemperature != null) {
573             updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new DecimalType(colorTemperature.getValue()));
574
575             @Nullable
576             Integer min = colorTemperature.getMin();
577             int colorMin = (min == null) ? 0 : min;
578
579             @Nullable
580             Integer max = colorTemperature.getMax();
581             int colorMax = (max == null) ? 0 : max;
582
583             colorTempPercent = (colorTemperature.getValue() - colorMin) / (colorMax - colorMin)
584                     * PercentType.HUNDRED.intValue();
585         }
586
587         updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(Float.toString(colorTempPercent)));
588         updateState(CHANNEL_EFFECT, new StringType(controllerInfo.getEffects().getSelect()));
589
590         @Nullable
591         Hue stateHue = state.getHue();
592         int hue = (stateHue != null) ? stateHue.getValue() : 0;
593         @Nullable
594         Sat stateSaturation = state.getSaturation();
595         int saturation = (stateSaturation != null) ? stateSaturation.getValue() : 0;
596         @Nullable
597         Brightness stateBrightness = state.getBrightness();
598         int brightness = (stateBrightness != null) ? stateBrightness.getValue() : 0;
599
600         updateState(CHANNEL_COLOR, new HSBType(new DecimalType(hue), new PercentType(saturation),
601                 new PercentType(powerState == OnOffType.ON ? brightness : 0)));
602         updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode()));
603         updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF);
604         updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode()));
605         updateState(CHANNEL_RHYTHM_STATE,
606                 controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF);
607         // update bridge properties which may have changed, or are not present during discovery
608         Map<String, String> properties = editProperties();
609         properties.put(Thing.PROPERTY_SERIAL_NUMBER, controllerInfo.getSerialNo());
610         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, controllerInfo.getFirmwareVersion());
611         properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
612         properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
613         updateProperties(properties);
614
615         Configuration config = editConfiguration();
616
617         if (MODEL_ID_CANVAS.equals(controllerInfo.getModel())) {
618             config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_CANVAS);
619             logger.debug("Set to device type {}", DEVICE_TYPE_CANVAS);
620         } else {
621             config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_LIGHTPANELS);
622             logger.debug("Set to device type {}", DEVICE_TYPE_LIGHTPANELS);
623         }
624         updateConfiguration(config);
625
626         getConfig().getProperties().forEach((key, value) -> {
627             logger.trace("Configuration property: key {} value {}", key, value);
628         });
629
630         getThing().getProperties().forEach((key, value) -> {
631             logger.debug("Thing property:  key {} value {}", key, value);
632         });
633
634         // update the color channels of each panel
635         this.getThing().getThings().forEach(child -> {
636             NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
637             if (panelHandler != null) {
638                 logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
639                 panelHandler.updatePanelColorChannel();
640             }
641         });
642     }
643
644     private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
645         ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient,
646                 getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET));
647         @Nullable
648         ControllerInfo controllerInfo = gson.fromJson(controllerlInfoJSON.getContentAsString(), ControllerInfo.class);
649         return controllerInfo;
650     }
651
652     private void sendStateCommand(String channel, Command command) throws NanoleafException {
653         State stateObject = new State();
654         switch (channel) {
655             case CHANNEL_POWER:
656                 if (command instanceof OnOffType) {
657                     // On/Off command - turns controller on/off
658                     BooleanState state = new On();
659                     state.setValue(OnOffType.ON.equals(command));
660                     stateObject.setState(state);
661                 } else {
662                     logger.warn("Unhandled command type: {}", command.getClass().getName());
663                     return;
664                 }
665                 break;
666             case CHANNEL_COLOR:
667                 if (command instanceof OnOffType) {
668                     // On/Off command - turns controller on/off
669                     BooleanState state = new On();
670                     state.setValue(OnOffType.ON.equals(command));
671                     stateObject.setState(state);
672                 } else if (command instanceof HSBType) {
673                     // regular color HSB command
674                     IntegerState h = new Hue();
675                     IntegerState s = new Sat();
676                     IntegerState b = new Brightness();
677                     h.setValue(((HSBType) command).getHue().intValue());
678                     s.setValue(((HSBType) command).getSaturation().intValue());
679                     b.setValue(((HSBType) command).getBrightness().intValue());
680                     stateObject.setState(h);
681                     stateObject.setState(s);
682                     stateObject.setState(b);
683                 } else if (command instanceof PercentType) {
684                     // brightness command
685                     IntegerState b = new Brightness();
686                     b.setValue(((PercentType) command).intValue());
687                     stateObject.setState(b);
688                 } else if (command instanceof IncreaseDecreaseType) {
689                     // increase/decrease brightness
690                     if (controllerInfo != null) {
691                         @Nullable
692                         Brightness brightness = controllerInfo.getState().getBrightness();
693                         int brightnessMin = 0;
694                         int brightnessMax = 0;
695                         if (brightness != null) {
696                             @Nullable
697                             Integer min = brightness.getMin();
698                             brightnessMin = (min == null) ? 0 : min;
699                             @Nullable
700                             Integer max = brightness.getMax();
701                             brightnessMax = (max == null) ? 0 : max;
702
703                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
704                                 brightness.setValue(
705                                         Math.min(brightnessMax, brightness.getValue() + BRIGHTNESS_STEP_SIZE));
706                             } else {
707                                 brightness.setValue(
708                                         Math.max(brightnessMin, brightness.getValue() - BRIGHTNESS_STEP_SIZE));
709                             }
710                             stateObject.setState(brightness);
711                             logger.debug("Setting controller brightness to {}", brightness.getValue());
712                             // update controller info in case new command is sent before next update job interval
713                             controllerInfo.getState().setBrightness(brightness);
714                         } else {
715                             logger.debug("Couldn't set brightness as it was null!");
716                         }
717                     }
718                 } else {
719                     logger.warn("Unhandled command type: {}", command.getClass().getName());
720                     return;
721                 }
722                 break;
723             case CHANNEL_COLOR_TEMPERATURE:
724                 if (command instanceof PercentType) {
725                     // Color temperature (percent)
726                     IntegerState state = new Ct();
727                     @Nullable
728                     Ct colorTemperature = controllerInfo.getState().getColorTemperature();
729
730                     int colorMin = 0;
731                     int colorMax = 0;
732                     if (colorTemperature != null) {
733                         @Nullable
734                         Integer min = colorTemperature.getMin();
735                         colorMin = (min == null) ? 0 : min;
736
737                         @Nullable
738                         Integer max = colorTemperature.getMax();
739                         colorMax = (max == null) ? 0 : max;
740                     }
741
742                     state.setValue(Math.round((colorMax - colorMin) * ((PercentType) command).intValue()
743                             / PercentType.HUNDRED.floatValue() + colorMin));
744                     stateObject.setState(state);
745                 } else {
746                     logger.warn("Unhandled command type: {}", command.getClass().getName());
747                     return;
748                 }
749                 break;
750             case CHANNEL_COLOR_TEMPERATURE_ABS:
751                 if (command instanceof DecimalType) {
752                     // Color temperature (absolute)
753                     IntegerState state = new Ct();
754                     state.setValue(((DecimalType) command).intValue());
755                     stateObject.setState(state);
756                 } else {
757                     logger.warn("Unhandled command type: {}", command.getClass().getName());
758                     return;
759                 }
760                 break;
761             case CHANNEL_PANEL_LAYOUT:
762                 @Nullable
763                 Layout layout = controllerInfo.getPanelLayout().getLayout();
764                 String layoutView = (layout != null) ? layout.getLayoutView() : "";
765                 logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
766                 updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF);
767                 break;
768             default:
769                 logger.warn("Unhandled command type: {}", command.getClass().getName());
770                 return;
771         }
772
773         Request setNewStateRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_SET_VALUE,
774                 HttpMethod.PUT);
775         setNewStateRequest.content(new StringContentProvider(gson.toJson(stateObject)), "application/json");
776         OpenAPIUtils.sendOpenAPIRequest(setNewStateRequest);
777     }
778
779     private void sendEffectCommand(Command command) throws NanoleafException {
780         Effects effects = new Effects();
781         if (command instanceof StringType) {
782             effects.setSelect(command.toString());
783         } else {
784             logger.warn("Unhandled command type: {}", command.getClass().getName());
785             return;
786         }
787         Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
788                 HttpMethod.PUT);
789         String content = gson.toJson(effects);
790         logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
791         setNewEffectRequest.content(new StringContentProvider(content), "application/json");
792         OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
793     }
794
795     private void sendRhythmCommand(Command command) throws NanoleafException {
796         Rhythm rhythm = new Rhythm();
797         if (command instanceof DecimalType) {
798             rhythm.setRhythmMode(((DecimalType) command).intValue());
799         } else {
800             logger.warn("Unhandled command type: {}", command.getClass().getName());
801             return;
802         }
803         Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_RHYTHM_MODE,
804                 HttpMethod.PUT);
805         setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
806         OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
807     }
808
809     private String getAddress() {
810         return StringUtils.defaultString(this.address);
811     }
812
813     private void setAddress(String address) {
814         this.address = address;
815     }
816
817     private int getPort() {
818         return port;
819     }
820
821     private void setPort(int port) {
822         this.port = port;
823     }
824
825     private int getRefreshIntervall() {
826         return refreshIntervall;
827     }
828
829     private void setRefreshIntervall(int refreshIntervall) {
830         this.refreshIntervall = refreshIntervall;
831     }
832
833     private String getAuthToken() {
834         return StringUtils.defaultString(authToken);
835     }
836
837     private void setAuthToken(@Nullable String authToken) {
838         this.authToken = authToken;
839     }
840
841     private String getDeviceType() {
842         return StringUtils.defaultString(deviceType);
843     }
844
845     private void setDeviceType(String deviceType) {
846         this.deviceType = deviceType;
847     }
848
849     private void stopAllJobs() {
850         stopPairingJob();
851         stopUpdateJob();
852         stopPanelDiscoveryJob();
853         stopTouchJob();
854     }
855 }