]> git.basschouten.com Git - openhab-addons.git/blob
304f9f62ab65b119bfc8865b8a69e14ec7ec0570
[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.io.IOException;
18 import java.net.URI;
19 import java.nio.charset.StandardCharsets;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collection;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.Scanner;
27 import java.util.concurrent.CopyOnWriteArrayList;
28 import java.util.concurrent.ExecutionException;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.TimeoutException;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.StringContentProvider;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.eclipse.jetty.http.HttpStatus;
41 import org.openhab.binding.nanoleaf.internal.NanoleafBadRequestException;
42 import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants;
43 import org.openhab.binding.nanoleaf.internal.NanoleafControllerListener;
44 import org.openhab.binding.nanoleaf.internal.NanoleafException;
45 import org.openhab.binding.nanoleaf.internal.NanoleafNotFoundException;
46 import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
47 import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
48 import org.openhab.binding.nanoleaf.internal.colors.NanoleafControllerColorChangeListener;
49 import org.openhab.binding.nanoleaf.internal.colors.NanoleafPanelColors;
50 import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider;
51 import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
52 import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService;
53 import org.openhab.binding.nanoleaf.internal.layout.ConstantPanelState;
54 import org.openhab.binding.nanoleaf.internal.layout.LayoutSettings;
55 import org.openhab.binding.nanoleaf.internal.layout.LivePanelState;
56 import org.openhab.binding.nanoleaf.internal.layout.NanoleafLayout;
57 import org.openhab.binding.nanoleaf.internal.layout.PanelState;
58 import org.openhab.binding.nanoleaf.internal.model.AuthToken;
59 import org.openhab.binding.nanoleaf.internal.model.BooleanState;
60 import org.openhab.binding.nanoleaf.internal.model.Brightness;
61 import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
62 import org.openhab.binding.nanoleaf.internal.model.Ct;
63 import org.openhab.binding.nanoleaf.internal.model.Effects;
64 import org.openhab.binding.nanoleaf.internal.model.Hue;
65 import org.openhab.binding.nanoleaf.internal.model.IntegerState;
66 import org.openhab.binding.nanoleaf.internal.model.Layout;
67 import org.openhab.binding.nanoleaf.internal.model.On;
68 import org.openhab.binding.nanoleaf.internal.model.PanelLayout;
69 import org.openhab.binding.nanoleaf.internal.model.PositionDatum;
70 import org.openhab.binding.nanoleaf.internal.model.Rhythm;
71 import org.openhab.binding.nanoleaf.internal.model.Sat;
72 import org.openhab.binding.nanoleaf.internal.model.State;
73 import org.openhab.binding.nanoleaf.internal.model.TouchEvents;
74 import org.openhab.binding.nanoleaf.internal.model.Write;
75 import org.openhab.core.config.core.Configuration;
76 import org.openhab.core.io.net.http.HttpClientFactory;
77 import org.openhab.core.library.types.DecimalType;
78 import org.openhab.core.library.types.HSBType;
79 import org.openhab.core.library.types.IncreaseDecreaseType;
80 import org.openhab.core.library.types.OnOffType;
81 import org.openhab.core.library.types.PercentType;
82 import org.openhab.core.library.types.QuantityType;
83 import org.openhab.core.library.types.RawType;
84 import org.openhab.core.library.types.StringType;
85 import org.openhab.core.library.unit.Units;
86 import org.openhab.core.thing.Bridge;
87 import org.openhab.core.thing.ChannelUID;
88 import org.openhab.core.thing.Thing;
89 import org.openhab.core.thing.ThingStatus;
90 import org.openhab.core.thing.ThingStatusDetail;
91 import org.openhab.core.thing.binding.BaseBridgeHandler;
92 import org.openhab.core.thing.binding.ThingHandlerCallback;
93 import org.openhab.core.thing.binding.ThingHandlerService;
94 import org.openhab.core.thing.util.ThingWebClientUtil;
95 import org.openhab.core.types.Command;
96 import org.openhab.core.types.RefreshType;
97 import org.slf4j.Logger;
98 import org.slf4j.LoggerFactory;
99
100 import com.google.gson.Gson;
101 import com.google.gson.JsonSyntaxException;
102
103 /**
104  * The {@link NanoleafControllerHandler} is responsible for handling commands to the controller which
105  * affect all panels connected to it (e.g. selected effect)
106  *
107  * @author Martin Raepple - Initial contribution
108  * @author Stefan Höhn - Canvas Touch Support
109  * @author Kai Kreuzer - refactoring, bug fixing and code clean up
110  */
111 @NonNullByDefault
112 public class NanoleafControllerHandler extends BaseBridgeHandler implements NanoleafControllerColorChangeListener {
113
114     // Pairing interval in seconds
115     private static final int PAIRING_INTERVAL = 10;
116     private static final int CONNECT_TIMEOUT = 10;
117
118     private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
119     private final HttpClientFactory httpClientFactory;
120     private final HttpClient httpClient;
121
122     private @Nullable HttpClient httpClientSSETouchEvent;
123     private @Nullable Request sseTouchjobRequest;
124     private final List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<NanoleafControllerListener>();
125     private PanelLayout previousPanelLayout = new PanelLayout();
126     private final NanoleafPanelColors panelColors = new NanoleafPanelColors();
127     private boolean updateVisualLayout = true;
128     private byte @Nullable [] layoutImage;
129
130     private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
131     private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
132     private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
133     private final Gson gson = new Gson();
134
135     private @Nullable String address;
136     private int port;
137     private int refreshIntervall;
138     private @Nullable String authToken;
139     private @Nullable String deviceType;
140     private @NonNullByDefault({}) ControllerInfo controllerInfo;
141
142     private boolean touchJobRunning = false;
143
144     public NanoleafControllerHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
145         super(bridge);
146         this.httpClientFactory = httpClientFactory;
147         this.httpClient = httpClientFactory.getCommonHttpClient();
148     }
149
150     private void initializeTouchHttpClient() {
151         String httpClientName = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null);
152
153         try {
154             httpClientSSETouchEvent = httpClientFactory.createHttpClient(httpClientName);
155             final HttpClient localHttpClientSSETouchEvent = this.httpClientSSETouchEvent;
156             if (localHttpClientSSETouchEvent != null) {
157                 localHttpClientSSETouchEvent.setConnectTimeout(CONNECT_TIMEOUT * 1000L);
158                 localHttpClientSSETouchEvent.start();
159             }
160         } catch (Exception e) {
161             logger.error(
162                     "Long running HttpClient for Nanoleaf controller handler {} cannot be started. Creating Handler failed.",
163                     httpClientName);
164             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
165         }
166
167         logger.debug("Using long SSE httpClient={} for {}}", httpClientSSETouchEvent, httpClientName);
168     }
169
170     @Override
171     public void initialize() {
172         logger.debug("Initializing the controller (bridge)");
173         this.panelColors.registerChangeListener(this);
174         updateStatus(ThingStatus.UNKNOWN);
175         NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
176         setAddress(config.address);
177         setPort(config.port);
178         setRefreshIntervall(config.refreshInterval);
179         String authToken = (config.authToken != null) ? config.authToken : "";
180         setAuthToken(authToken);
181         Map<String, String> properties = getThing().getProperties();
182         String propertyModelId = properties.get(Thing.PROPERTY_MODEL_ID);
183         if (hasTouchSupport(propertyModelId)) {
184             config.deviceType = DEVICE_TYPE_TOUCHSUPPORT;
185             initializeTouchHttpClient();
186         } else {
187             config.deviceType = DEVICE_TYPE_LIGHTPANELS;
188         }
189
190         setDeviceType(config.deviceType);
191         String propertyFirmwareVersion = properties.get(Thing.PROPERTY_FIRMWARE_VERSION);
192
193         try {
194             if (!config.address.isEmpty() && !String.valueOf(config.port).isEmpty()) {
195                 if (propertyFirmwareVersion != null && !propertyFirmwareVersion.isEmpty() && !OpenAPIUtils
196                         .checkRequiredFirmware(properties.get(Thing.PROPERTY_MODEL_ID), propertyFirmwareVersion)) {
197                     logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
198                             propertyFirmwareVersion, API_MIN_FW_VER_LIGHTPANELS);
199                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
200                             "@text/error.nanoleaf.controller.incompatibleFirmware");
201                     stopAllJobs();
202                 } else if (authToken != null && !authToken.isEmpty()) {
203                     stopPairingJob();
204                     startUpdateJob();
205                     startTouchJob();
206                 } else {
207                     logger.debug("No token found. Start pairing background job");
208                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
209                             "@text/error.nanoleaf.controller.noToken");
210                     startPairingJob();
211                     stopUpdateJob();
212                 }
213             } else {
214                 logger.warn("No IP address and port configured for the Nanoleaf controller");
215                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
216                         "@text/error.nanoleaf.controller.noIp");
217                 stopAllJobs();
218             }
219         } catch (IllegalArgumentException iae) {
220             logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
221                     getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION));
222             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
223                     "@text/error.nanoleaf.controller.incompatibleFirmware");
224         }
225     }
226
227     @Override
228     public void handleCommand(ChannelUID channelUID, Command command) {
229         logger.debug("Received command {} for channel {}", command, channelUID);
230         if (!ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) {
231             logger.debug("Cannot handle command. Bridge is not online.");
232         } else {
233             try {
234                 if (command instanceof RefreshType) {
235                     updateFromControllerInfo();
236                 } else {
237                     switch (channelUID.getId()) {
238                         case CHANNEL_COLOR:
239                         case CHANNEL_COLOR_TEMPERATURE:
240                         case CHANNEL_COLOR_TEMPERATURE_ABS:
241                             sendStateCommand(channelUID.getId(), command);
242                             break;
243                         case CHANNEL_EFFECT:
244                             sendEffectCommand(command);
245                             break;
246                         case CHANNEL_RHYTHM_MODE:
247                             sendRhythmCommand(command);
248                             break;
249                         default:
250                             logger.warn("Channel with id {} not handled", channelUID.getId());
251                             break;
252                     }
253                 }
254             } catch (NanoleafUnauthorizedException nue) {
255                 logger.debug("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
256                         nue.getMessage());
257                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
258                         "@text/error.nanoleaf.controller.invalidToken");
259             } catch (NanoleafException ne) {
260                 logger.debug("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
261                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
262                         "@text/error.nanoleaf.controller.communication");
263             }
264         }
265     }
266
267     @Override
268     public void handleRemoval() {
269         scheduler.execute(() -> {
270             try {
271                 Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
272                         API_DELETE_USER, HttpMethod.DELETE);
273                 ContentResponse deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
274                 if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
275                     logger.warn("Failed to delete token for openHAB. Response code is {}",
276                             deleteTokenResponse.getStatus());
277                     return;
278                 }
279                 logger.debug("Successfully deleted token for openHAB from controller");
280             } catch (NanoleafUnauthorizedException e) {
281                 logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
282             } catch (NanoleafException ne) {
283                 logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
284             }
285             stopAllJobs();
286             super.handleRemoval();
287             logger.debug("Nanoleaf controller removed");
288         });
289     }
290
291     @Override
292     public void dispose() {
293         stopAllJobs();
294         HttpClient localHttpClientSSETouchEvent = this.httpClientSSETouchEvent;
295         if (localHttpClientSSETouchEvent != null) {
296             try {
297                 localHttpClientSSETouchEvent.stop();
298             } catch (Exception e) {
299             }
300             this.httpClientSSETouchEvent = null;
301         }
302         super.dispose();
303         logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
304     }
305
306     @Override
307     public Collection<Class<? extends ThingHandlerService>> getServices() {
308         return List.of(NanoleafPanelsDiscoveryService.class, NanoleafCommandDescriptionProvider.class);
309     }
310
311     public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
312         logger.debug("Register new listener for controller {}", getThing().getUID());
313         return controllerListeners.add(controllerListener);
314     }
315
316     public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
317         logger.debug("Unregister listener for controller {}", getThing().getUID());
318         return controllerListeners.remove(controllerListener);
319     }
320
321     public NanoleafControllerConfig getControllerConfig() {
322         NanoleafControllerConfig config = new NanoleafControllerConfig();
323         config.address = Objects.requireNonNullElse(getAddress(), "");
324         config.port = getPort();
325         config.refreshInterval = getRefreshInterval();
326         config.authToken = getAuthToken();
327         config.deviceType = Objects.requireNonNullElse(getDeviceType(), "");
328         return config;
329     }
330
331     public String getLayout() {
332         String layoutView = "";
333         if (controllerInfo != null) {
334             PanelLayout panelLayout = controllerInfo.getPanelLayout();
335             Layout layout = panelLayout.getLayout();
336             layoutView = layout != null ? layout.getLayoutView() : "";
337         }
338
339         return layoutView;
340     }
341
342     public synchronized void startPairingJob() {
343         if (pairingJob == null || pairingJob.isCancelled()) {
344             logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
345             pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0L, PAIRING_INTERVAL, TimeUnit.SECONDS);
346         }
347     }
348
349     private synchronized void stopPairingJob() {
350         logger.debug("Stop pairing job {}", pairingJob != null ? pairingJob.isCancelled() : "pairing job = null");
351         if (pairingJob != null && !pairingJob.isCancelled()) {
352             pairingJob.cancel(true);
353             pairingJob = null;
354             logger.debug("Stopped pairing job");
355         }
356     }
357
358     private synchronized void startUpdateJob() {
359         final String localAuthToken = getAuthToken();
360         if (localAuthToken != null && !localAuthToken.isEmpty()) {
361             if (updateJob == null || updateJob.isCancelled()) {
362                 logger.debug("Start controller status job, repeat every {} sec", getRefreshInterval());
363                 updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0L, getRefreshInterval(),
364                         TimeUnit.SECONDS);
365             }
366         } else {
367             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
368                     "@text/error.nanoleaf.controller.noToken");
369         }
370     }
371
372     private synchronized void stopUpdateJob() {
373         logger.debug("Stop update job {}", updateJob != null ? updateJob.isCancelled() : "update job = null");
374         if (updateJob != null && !updateJob.isCancelled()) {
375             updateJob.cancel(true);
376             updateJob = null;
377             logger.debug("Stopped status job");
378         }
379     }
380
381     private synchronized void startTouchJob() {
382         NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
383         if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) {
384             logger.debug(
385                     "NOT starting TouchJob for Controller {} because it has wrong device type '{}' vs required '{}'",
386                     this.getThing().getUID(), config.deviceType, DEVICE_TYPE_TOUCHSUPPORT);
387         } else {
388             logger.debug("Starting TouchJob for Controller {}", getThing().getUID());
389             final String localAuthToken = getAuthToken();
390             if (localAuthToken != null && !localAuthToken.isEmpty()) {
391                 if (touchJob != null && !touchJob.isDone()) {
392                     logger.trace("tj: tj={} already running touchJobRunning = {}  cancelled={} done={}", touchJob,
393                             touchJobRunning, touchJob == null ? null : touchJob.isCancelled(),
394                             touchJob == null ? null : touchJob.isDone());
395                 } else {
396                     logger.debug("tj: Starting NEW touch job : tj={} touchJobRunning={} cancelled={}  done={}",
397                             touchJob, touchJobRunning, touchJob == null ? null : touchJob.isCancelled(),
398                             touchJob == null ? null : touchJob.isDone());
399                     touchJob = scheduler.scheduleWithFixedDelay(this::runTouchDetection, 0L, 1L, TimeUnit.SECONDS);
400                 }
401             } else {
402                 logger.error("starting TouchJob for Controller {} failed - missing token", getThing().getUID());
403             }
404
405         }
406     }
407
408     private synchronized void stopTouchJob() {
409         logger.debug("Stop touch job {}", touchJob != null ? touchJob.isCancelled() : "touchJob job = null");
410         if (touchJob != null) {
411             logger.trace("tj: touch job stopping for {} with client {}", thing.getUID(), httpClientSSETouchEvent);
412
413             final Request localSSERequest = sseTouchjobRequest;
414             if (localSSERequest != null) {
415                 localSSERequest.abort(new NanoleafException("Touch detection stopped"));
416             }
417             if (!touchJob.isCancelled()) {
418                 touchJob.cancel(true);
419             }
420
421             touchJob = null;
422             touchJobRunning = false;
423             logger.debug("tj: touch job stopped for {} with client {}", thing.getUID(), httpClientSSETouchEvent);
424         }
425     }
426
427     private boolean hasTouchSupport(@Nullable String deviceType) {
428         return NanoleafBindingConstants.MODELS_WITH_TOUCHSUPPORT.contains(deviceType);
429     }
430
431     private void runUpdate() {
432         logger.debug("Run update job");
433
434         try {
435             updateFromControllerInfo();
436             startTouchJob();
437             updateStatus(ThingStatus.ONLINE);
438         } catch (NanoleafUnauthorizedException nae) {
439             logger.debug("Status update unauthorized for controller {}: {}", getThing().getUID(), nae.getMessage());
440             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
441                     "@text/error.nanoleaf.controller.invalidToken");
442             final String localAuthToken = getAuthToken();
443             if (localAuthToken == null || localAuthToken.isEmpty()) {
444                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
445                         "@text/error.nanoleaf.controller.noToken");
446             }
447         } catch (NanoleafException ne) {
448             logger.debug("Status update failed for controller {} : {}", getThing().getUID(), ne.getMessage());
449             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
450                     "@text/error.nanoleaf.controller.communication");
451         } catch (RuntimeException e) {
452             logger.debug("Update job failed", e);
453             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
454         }
455     }
456
457     private void runPairing() {
458         logger.debug("Run pairing job");
459
460         try {
461             final String localAuthToken = getAuthToken();
462             if (localAuthToken != null && !localAuthToken.isEmpty()) {
463                 if (pairingJob != null) {
464                     pairingJob.cancel(false);
465                 }
466
467                 logger.debug("Authentication token found. Canceling pairing job");
468                 return;
469             }
470
471             ContentResponse authTokenResponse = OpenAPIUtils
472                     .requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST)
473                     .timeout(20L, TimeUnit.SECONDS).send();
474             String authTokenResponseString = (authTokenResponse != null) ? authTokenResponse.getContentAsString() : "";
475             if (logger.isTraceEnabled()) {
476                 logger.trace("Auth token response: {}", authTokenResponseString);
477             }
478
479             if (authTokenResponse != null && authTokenResponse.getStatus() != HttpStatus.OK_200) {
480                 logger.debug("Pairing pending for {}. Controller returns status code {}", getThing().getUID(),
481                         authTokenResponse.getStatus());
482             } else {
483                 AuthToken authTokenObject = gson.fromJson(authTokenResponseString, AuthToken.class);
484                 authTokenObject = (authTokenObject != null) ? authTokenObject : new AuthToken();
485                 if (authTokenObject.getAuthToken().isEmpty()) {
486                     logger.debug("No auth token found in response: {}", authTokenResponseString);
487                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
488                             "@text/error.nanoleaf.controller.pairingFailed");
489                     throw new NanoleafException(authTokenResponseString);
490                 }
491
492                 logger.debug("Pairing succeeded.");
493                 Configuration config = editConfiguration();
494
495                 config.put(NanoleafControllerConfig.AUTH_TOKEN, authTokenObject.getAuthToken());
496                 updateConfiguration(config);
497                 updateStatus(ThingStatus.ONLINE);
498                 // Update local field
499                 setAuthToken(authTokenObject.getAuthToken());
500
501                 stopPairingJob();
502                 startUpdateJob();
503                 startTouchJob();
504             }
505         } catch (JsonSyntaxException e) {
506             logger.warn("Received invalid data", e);
507             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
508                     "@text/error.nanoleaf.controller.invalidData");
509         } catch (NanoleafException ne) {
510             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
511                     "@text/error.nanoleaf.controller.noTokenReceived");
512         } catch (ExecutionException | TimeoutException | InterruptedException e) {
513             logger.debug("Cannot send authorization request to controller: ", e);
514             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
515                     "@text/error.nanoleaf.controller.authRequest");
516         } catch (RuntimeException e) {
517             logger.warn("Pairing job failed", e);
518             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
519         } catch (Exception e) {
520             logger.warn("Cannot start http client", e);
521             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
522                     "@text/error.nanoleaf.controller.noClient");
523         }
524     }
525
526     private synchronized void runTouchDetection() {
527         final HttpClient localhttpSSEClientTouchEvent = httpClientSSETouchEvent;
528         int eventHashcode = -1;
529         if (localhttpSSEClientTouchEvent != null) {
530             eventHashcode = localhttpSSEClientTouchEvent.hashCode();
531         }
532         if (touchJobRunning) {
533             logger.trace("tj: touch job {} touch job already running. quitting. {} controller {} with {}\",\n",
534                     touchJob, eventHashcode, thing.getUID(), httpClientSSETouchEvent);
535         } else {
536             try {
537                 URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
538                 logger.debug("tj: touch job request registering for {} with client {}", thing.getUID(),
539                         httpClientSSETouchEvent);
540                 touchJobRunning = true;
541                 if (localhttpSSEClientTouchEvent != null) {
542                     localhttpSSEClientTouchEvent.setIdleTimeout(CONNECT_TIMEOUT * 1000L);
543                     sseTouchjobRequest = localhttpSSEClientTouchEvent.newRequest(eventUri);
544                     final Request localSSETouchjobRequest = sseTouchjobRequest;
545                     if (localSSETouchjobRequest != null) {
546                         int requestHashCode = localSSETouchjobRequest.hashCode();
547
548                         logger.debug("tj: triggering new touch job request {} for {} with client {}", requestHashCode,
549                                 thing.getUID(), eventHashcode);
550                         localSSETouchjobRequest.onResponseContent((response, content) -> {
551                             String s = StandardCharsets.UTF_8.decode(content).toString();
552                             logger.debug("touch detected for controller {}", thing.getUID());
553                             logger.trace("content {}", s);
554                             try (Scanner eventContent = new Scanner(s)) {
555                                 while (eventContent.hasNextLine()) {
556                                     String line = eventContent.nextLine().trim();
557                                     if (line.startsWith("data:")) {
558                                         String json = line.substring(5).trim();
559
560                                         try {
561                                             TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
562                                             handleTouchEvents(Objects.requireNonNull(touchEvents));
563                                         } catch (JsonSyntaxException e) {
564                                             logger.error("Couldn't parse touch event json {}", json);
565                                         }
566                                     }
567                                 }
568                             }
569                             logger.debug("leaving touch onContent");
570                         }).onResponseSuccess((response) -> {
571                             logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response);
572                         }).onResponseFailure((response, failure) -> {
573                             logger.trace("tj: r={} touch event FAILURE. Touchjob not running anymore for controller {}",
574                                     response.getRequest(), thing.getUID());
575                         }).send((result) -> {
576                             logger.trace(
577                                     "tj: r={} touch event COMPLETE. Touchjob not running anymore for controller {}      failed: {}        succeeded: {}",
578                                     result.getRequest(), thing.getUID(), result.isFailed(), result.isSucceeded());
579                             touchJobRunning = false;
580                         });
581                     }
582                 }
583                 logger.trace("tj: started touch job request for {} with {} at {}", thing.getUID(),
584                         httpClientSSETouchEvent, eventUri);
585             } catch (NanoleafException | RuntimeException e) {
586                 logger.warn("tj: setting up TouchDetection failed for controller {} with {}\",\n", thing.getUID(),
587                         httpClientSSETouchEvent);
588                 logger.warn("tj: setting up TouchDetection failed with exception", e);
589             } finally {
590                 logger.trace("tj: touch job {} started for new request {} controller {} with {}\",\n",
591                         touchJob.hashCode(), eventHashcode, thing.getUID(), httpClientSSETouchEvent);
592             }
593
594         }
595     }
596
597     private void handleTouchEvents(TouchEvents touchEvents) {
598         touchEvents.getEvents().forEach((event) -> {
599             logger.debug("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
600             // Swipes go to the controller, taps go to the individual panel
601             if (event.getPanelId().equals(CONTROLLER_PANEL_ID)) {
602                 logger.debug("Triggering controller {} with gesture {}.", thing.getUID(), event.getGesture());
603                 updateControllerGesture(event.getGesture());
604             } else {
605                 getThing().getThings().forEach((child) -> {
606                     NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
607                     if (panelHandler != null) {
608                         logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
609                                 event.getPanelId());
610                         if (panelHandler.getPanelID().equals(Integer.valueOf(event.getPanelId()))) {
611                             logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
612                                     event.getGesture());
613                             panelHandler.updatePanelGesture(event.getGesture());
614                         }
615                     }
616
617                 });
618             }
619         });
620     }
621
622     /**
623      * Apply the swipe gesture to the controller
624      *
625      * @param gesture Only swipes are supported on the complete nanoleaf panels
626      */
627     private void updateControllerGesture(int gesture) {
628         switch (gesture) {
629             case 2:
630                 triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_UP);
631                 break;
632             case 3:
633                 triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_DOWN);
634                 break;
635             case 4:
636                 triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_LEFT);
637                 break;
638             case 5:
639                 triggerChannel(CHANNEL_SWIPE, CHANNEL_SWIPE_EVENT_RIGHT);
640                 break;
641         }
642     }
643
644     private void updateFromControllerInfo() throws NanoleafException {
645         logger.debug("Update channels for controller {}", thing.getUID());
646         controllerInfo = receiveControllerInfo();
647         State state = controllerInfo.getState();
648
649         OnOffType powerState = state.getOnOff();
650
651         Ct colorTemperature = state.getColorTemperature();
652
653         float colorTempPercent = 0.0F;
654         int hue;
655         int saturation;
656         if (colorTemperature != null) {
657             updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new QuantityType(colorTemperature.getValue(), Units.KELVIN));
658             Integer min = colorTemperature.getMin();
659             hue = min == null ? 0 : min;
660             Integer max = colorTemperature.getMax();
661             saturation = max == null ? 0 : max;
662             colorTempPercent = (colorTemperature.getValue() - hue) / (saturation - hue)
663                     * PercentType.HUNDRED.intValue();
664         }
665
666         updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(Float.toString(colorTempPercent)));
667         updateState(CHANNEL_EFFECT, new StringType(controllerInfo.getEffects().getSelect()));
668         Hue stateHue = state.getHue();
669         hue = stateHue != null ? stateHue.getValue() : 0;
670
671         Sat stateSaturation = state.getSaturation();
672         saturation = stateSaturation != null ? stateSaturation.getValue() : 0;
673
674         Brightness stateBrightness = state.getBrightness();
675         int brightness = stateBrightness != null ? stateBrightness.getValue() : 0;
676         HSBType stateColor = new HSBType(new DecimalType(hue), new PercentType(saturation),
677                 new PercentType(powerState == OnOffType.ON ? brightness : 0));
678
679         updateState(CHANNEL_COLOR, stateColor);
680         updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode()));
681         updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF);
682         updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode()));
683         updateState(CHANNEL_RHYTHM_STATE,
684                 controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF);
685
686         updatePanelColors();
687         if (EFFECT_NAME_SOLID_COLOR.equals(controllerInfo.getEffects().getSelect())) {
688             setSolidColor(stateColor);
689         }
690         updateProperties();
691         updateConfiguration();
692         updateLayout(controllerInfo.getPanelLayout());
693         updateVisualState(controllerInfo.getPanelLayout(), powerState);
694
695         for (NanoleafControllerListener controllerListener : controllerListeners) {
696             controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
697         }
698     }
699
700     private void setSolidColor(HSBType color) {
701         // If the panels are set to solid color, they are read from the state
702         PanelLayout panelLayout = controllerInfo.getPanelLayout();
703         Layout layout = panelLayout.getLayout();
704
705         if (layout != null) {
706             List<PositionDatum> positionData = layout.getPositionData();
707             if (positionData != null) {
708                 List<Integer> allPanelIds = new ArrayList<>(positionData.size());
709                 for (PositionDatum pd : positionData) {
710                     allPanelIds.add(pd.getPanelId());
711                 }
712
713                 panelColors.setMultiple(allPanelIds, color);
714             } else {
715                 logger.debug("Missing position datum when setting solid color for {}", getThing().getUID());
716             }
717         } else {
718             logger.debug("Missing layout when setting solid color for {}", getThing().getUID());
719         }
720     }
721
722     private void updateConfiguration() {
723         // only update the Thing config if value isn't set yet
724         if (getConfig().get(NanoleafControllerConfig.DEVICE_TYPE) == null) {
725             Configuration config = editConfiguration();
726             if (hasTouchSupport(controllerInfo.getModel())) {
727                 config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_TOUCHSUPPORT);
728                 logger.debug("Set to device type {}", DEVICE_TYPE_TOUCHSUPPORT);
729             } else {
730                 config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_LIGHTPANELS);
731                 logger.debug("Set to device type {}", DEVICE_TYPE_LIGHTPANELS);
732             }
733             updateConfiguration(config);
734             if (logger.isTraceEnabled()) {
735                 getConfig().getProperties().forEach((key, value) -> {
736                     logger.trace("Configuration property: key {} value {}", key, value);
737                 });
738             }
739         }
740     }
741
742     private void updateProperties() {
743         // update bridge properties which may have changed, or are not present during discovery
744         Map<String, String> properties = editProperties();
745         properties.put(Thing.PROPERTY_SERIAL_NUMBER, controllerInfo.getSerialNo());
746         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, controllerInfo.getFirmwareVersion());
747         properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
748         properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
749         updateProperties(properties);
750         if (logger.isTraceEnabled()) {
751             getThing().getProperties().forEach((key, value) -> {
752                 logger.trace("Thing property: key {} value {}", key, value);
753             });
754         }
755     }
756
757     private void updateVisualState(PanelLayout panelLayout, OnOffType powerState) {
758         ChannelUID stateChannel = new ChannelUID(getThing().getUID(), CHANNEL_VISUAL_STATE);
759
760         try {
761             PanelState panelState;
762             if (OnOffType.OFF.equals(powerState)) {
763                 // If powered off: show all panels as black
764                 panelState = new ConstantPanelState(HSBType.BLACK);
765             } else {
766                 // Static color for panels, use it
767                 panelState = new LivePanelState(panelColors);
768             }
769
770             LayoutSettings settings = new LayoutSettings(false, true, true, true);
771             byte[] bytes = NanoleafLayout.render(panelLayout, panelState, settings);
772             if (bytes.length > 0) {
773                 updateState(stateChannel, new RawType(bytes, "image/png"));
774                 logger.trace("Rendered visual state of panel {} in updateState has {} bytes", getThing().getUID(),
775                         bytes.length);
776             } else {
777                 logger.debug("Visual state of {} failed to produce any image", getThing().getUID());
778             }
779         } catch (IOException ioex) {
780             logger.warn("Failed to create state image", ioex);
781         }
782     }
783
784     private void updateLayout(PanelLayout panelLayout) {
785         ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT);
786         ThingHandlerCallback callback = getCallback();
787         if (callback != null) {
788             if (!callback.isChannelLinked(layoutChannel)) {
789                 // Don't generate image unless it is used
790                 return;
791             }
792         }
793
794         if (layoutImage != null && previousPanelLayout.equals(panelLayout)) {
795             logger.trace("Not rendering panel layout for {} as it is the same as previous rendered panel layout",
796                     getThing().getUID());
797             return;
798         }
799
800         try {
801             LayoutSettings settings = new LayoutSettings(true, false, true, false);
802             byte[] bytes = NanoleafLayout.render(panelLayout, new LivePanelState(panelColors), settings);
803             if (bytes.length > 0) {
804                 updateState(layoutChannel, new RawType(bytes, "image/png"));
805                 layoutImage = bytes;
806                 previousPanelLayout = panelLayout;
807                 logger.trace("Rendered layout of panel {} in updateState has {} bytes", getThing().getUID(),
808                         bytes.length);
809             } else {
810                 logger.debug("Layout of {} failed to produce any image", getThing().getUID());
811             }
812
813         } catch (IOException ioex) {
814             logger.warn("Failed to create layout image", ioex);
815         }
816     }
817
818     private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
819         ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient,
820                 getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET));
821         ControllerInfo controllerInfo = gson.fromJson(controllerlInfoJSON.getContentAsString(), ControllerInfo.class);
822         return Objects.requireNonNull(controllerInfo);
823     }
824
825     private void sendStateCommand(String channel, Command command) throws NanoleafException {
826         State stateObject = new State();
827         switch (channel) {
828             case CHANNEL_COLOR:
829                 if (command instanceof OnOffType) {
830                     // On/Off command - turns controller on/off
831                     BooleanState state = new On();
832                     state.setValue(OnOffType.ON.equals(command));
833                     stateObject.setState(state);
834                 } else if (command instanceof HSBType) {
835                     // regular color HSB command
836                     IntegerState h = new Hue();
837                     IntegerState s = new Sat();
838                     IntegerState b = new Brightness();
839                     h.setValue(((HSBType) command).getHue().intValue());
840                     s.setValue(((HSBType) command).getSaturation().intValue());
841                     b.setValue(((HSBType) command).getBrightness().intValue());
842                     setSolidColor((HSBType) command);
843                     stateObject.setState(h);
844                     stateObject.setState(s);
845                     stateObject.setState(b);
846                 } else if (command instanceof PercentType) {
847                     // brightness command
848                     IntegerState b = new Brightness();
849                     b.setValue(((PercentType) command).intValue());
850                     stateObject.setState(b);
851                 } else if (command instanceof IncreaseDecreaseType) {
852                     // increase/decrease brightness
853                     if (controllerInfo != null) {
854                         @Nullable
855                         Brightness brightness = controllerInfo.getState().getBrightness();
856                         int brightnessMin;
857                         int brightnessMax;
858                         if (brightness != null) {
859                             @Nullable
860                             Integer min = brightness.getMin();
861                             brightnessMin = (min == null) ? 0 : min;
862                             @Nullable
863                             Integer max = brightness.getMax();
864                             brightnessMax = (max == null) ? 0 : max;
865
866                             if (IncreaseDecreaseType.INCREASE.equals(command)) {
867                                 brightness.setValue(
868                                         Math.min(brightnessMax, brightness.getValue() + BRIGHTNESS_STEP_SIZE));
869                             } else {
870                                 brightness.setValue(
871                                         Math.max(brightnessMin, brightness.getValue() - BRIGHTNESS_STEP_SIZE));
872                             }
873                             stateObject.setState(brightness);
874                             logger.debug("Setting controller brightness to {}", brightness.getValue());
875                             // update controller info in case new command is sent before next update job interval
876                             controllerInfo.getState().setBrightness(brightness);
877                         } else {
878                             logger.debug("Couldn't set brightness as it was null!");
879                         }
880                     }
881                 } else {
882                     logger.warn("Unhandled command {} with command type: {}", command, command.getClass().getName());
883                     return;
884                 }
885                 break;
886             case CHANNEL_COLOR_TEMPERATURE:
887                 if (command instanceof PercentType) {
888                     // Color temperature (percent)
889                     IntegerState state = new Ct();
890                     @Nullable
891                     Ct colorTemperature = controllerInfo.getState().getColorTemperature();
892
893                     int colorMin = 0;
894                     int colorMax = 0;
895                     if (colorTemperature != null) {
896                         @Nullable
897                         Integer min = colorTemperature.getMin();
898                         colorMin = (min == null) ? 0 : min;
899
900                         @Nullable
901                         Integer max = colorTemperature.getMax();
902                         colorMax = (max == null) ? 0 : max;
903                     }
904
905                     state.setValue(Math.round((colorMax - colorMin) * (100 - ((PercentType) command).intValue())
906                             / PercentType.HUNDRED.floatValue() + colorMin));
907                     stateObject.setState(state);
908                 } else {
909                     logger.warn("Unhandled command type: {}", command.getClass().getName());
910                     return;
911                 }
912                 break;
913             case CHANNEL_COLOR_TEMPERATURE_ABS:
914                 // Color temperature (absolute)
915                 IntegerState state = new Ct();
916                 if (command instanceof DecimalType) {
917                     state.setValue(((DecimalType) command).intValue());
918                 } else if (command instanceof QuantityType) {
919                     QuantityType<?> tempKelvin = ((QuantityType) command).toInvertibleUnit(Units.KELVIN);
920                     if (tempKelvin == null) {
921                         logger.warn("Cannot convert color temperature {} to Kelvin.", command);
922                         return;
923                     }
924                     state.setValue(tempKelvin.intValue());
925                 } else {
926                     logger.warn("Unhandled command type: {}", command.getClass().getName());
927                     return;
928                 }
929
930                 stateObject.setState(state);
931                 break;
932             default:
933                 logger.warn("Unhandled command type: {}", command.getClass().getName());
934                 return;
935         }
936
937         Request setNewStateRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_SET_VALUE,
938                 HttpMethod.PUT);
939         setNewStateRequest.content(new StringContentProvider(gson.toJson(stateObject)), "application/json");
940         OpenAPIUtils.sendOpenAPIRequest(setNewStateRequest);
941     }
942
943     private void sendEffectCommand(Command command) throws NanoleafException {
944         Effects effects = new Effects();
945         if (command instanceof StringType) {
946             effects.setSelect(command.toString());
947             Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
948                     HttpMethod.PUT);
949             String content = gson.toJson(effects);
950             logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
951             setNewEffectRequest.content(new StringContentProvider(content), "application/json");
952             OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
953         } else {
954             logger.warn("Unhandled command type: {}", command.getClass().getName());
955         }
956     }
957
958     private void sendRhythmCommand(Command command) throws NanoleafException {
959         Rhythm rhythm = new Rhythm();
960         if (command instanceof DecimalType) {
961             rhythm.setRhythmMode(((DecimalType) command).intValue());
962             Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
963                     API_RHYTHM_MODE, HttpMethod.PUT);
964             setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
965             OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
966         } else {
967             logger.warn("Unhandled command type: {}", command.getClass().getName());
968         }
969     }
970
971     private boolean hasStaticEffect() {
972         return EFFECT_NAME_STATIC_COLOR.equals(controllerInfo.getEffects().getSelect())
973                 || EFFECT_NAME_SOLID_COLOR.equals(controllerInfo.getEffects().getSelect());
974     }
975
976     /**
977      * Checks if we are in a mode where color changes should be rendered.
978      *
979      * @return True if a color change on a panel should be rendered
980      */
981     private boolean showsUpdatedColors() {
982         if (!hasStaticEffect()) {
983             logger.trace("Not updating colors as the device doesnt have a static/solid effect");
984             return false;
985         }
986
987         State state = controllerInfo.getState();
988         OnOffType powerState = state.getOnOff();
989         return OnOffType.ON.equals(powerState);
990     }
991
992     @Override
993     public void onPanelChangedColor() {
994         if (updateVisualLayout && showsUpdatedColors()) {
995             // Update the visual state if a panel has changed color
996             updateVisualState(controllerInfo.getPanelLayout(), controllerInfo.getState().getOnOff());
997         } else {
998             logger.trace("Not updating colors. Update visual layout:  {}", updateVisualLayout);
999         }
1000     }
1001
1002     /**
1003      * For individual panels to get access to the panel colors.
1004      *
1005      * @return Information about colors of panels.
1006      */
1007     public NanoleafPanelColors getColorInformation() {
1008         return panelColors;
1009     }
1010
1011     private void updatePanelColors() {
1012         // get panel color data from controller
1013         try {
1014             Effects effects = new Effects();
1015             Write write = new Write();
1016             write.setCommand("request");
1017             write.setAnimName(EFFECT_NAME_STATIC_COLOR);
1018             effects.setWrite(write);
1019
1020             NanoleafControllerConfig config = getControllerConfig();
1021             logger.debug("Sending Request from Panel for getColor()");
1022             Request setPanelUpdateRequest = OpenAPIUtils.requestBuilder(httpClient, config, API_EFFECT, HttpMethod.PUT);
1023             setPanelUpdateRequest.content(new StringContentProvider(gson.toJson(effects)), "application/json");
1024             ContentResponse panelData = OpenAPIUtils.sendOpenAPIRequest(setPanelUpdateRequest);
1025             // parse panel data
1026             parsePanelData(config, panelData);
1027
1028         } catch (NanoleafNotFoundException nfe) {
1029             logger.debug("Panel data could not be retrieved as no data was returned (static type missing?) : {}",
1030                     nfe.getMessage());
1031         } catch (NanoleafBadRequestException nfe) {
1032             logger.debug(
1033                     "Panel data could not be retrieved as request not expected(static type missing / dynamic type on) : {}",
1034                     nfe.getMessage());
1035         } catch (NanoleafException nue) {
1036             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
1037                     "@text/error.nanoleaf.panel.communication");
1038             logger.debug("Panel data could not be retrieved: {}", nue.getMessage());
1039         }
1040     }
1041
1042     void parsePanelData(NanoleafControllerConfig config, ContentResponse panelData) {
1043         // panelData is in format (numPanels, (PanelId, 1, R, G, B, W, TransitionTime) * numPanel)
1044         @Nullable
1045         Write response = null;
1046
1047         String panelDataContent = panelData.getContentAsString();
1048         try {
1049             response = gson.fromJson(panelDataContent, Write.class);
1050         } catch (JsonSyntaxException jse) {
1051             logger.warn("Unable to parse panel data information from Nanoleaf", jse);
1052             logger.trace("Panel Data which couldn't be parsed: {}", panelDataContent);
1053         }
1054
1055         if (response != null) {
1056             try {
1057                 updateVisualLayout = false;
1058                 String[] tokenizedData = response.getAnimData().split(" ");
1059                 if (config.deviceType.equals(CONFIG_DEVICE_TYPE_LIGHTPANELS)
1060                         || config.deviceType.equals(CONFIG_DEVICE_TYPE_CANVAS)) {
1061                     // panelData is in format (numPanels (PanelId 1 R G B W TransitionTime) * numPanel)
1062                     String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 1, tokenizedData.length);
1063                     for (int i = 0; i < panelDataPoints.length; i++) {
1064                         if (i % 7 == 0) {
1065                             // found panel data - store it
1066                             panelColors.setPanelColor(Integer.valueOf(panelDataPoints[i]),
1067                                     HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 2]),
1068                                             Integer.parseInt(panelDataPoints[i + 3]),
1069                                             Integer.parseInt(panelDataPoints[i + 4])));
1070                         }
1071                     }
1072                 } else {
1073                     // panelData is in format (0 numPanels (quotient(panelID) remainder(panelID) R G B W 0
1074                     // quotient(TransitionTime) remainder(TransitionTime)) * numPanel)
1075                     String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length);
1076                     for (int i = 0; i < panelDataPoints.length; i++) {
1077                         if (i % 8 == 0) {
1078                             Integer idQuotient = Integer.valueOf(panelDataPoints[i]);
1079                             Integer idRemainder = Integer.valueOf(panelDataPoints[i + 1]);
1080                             Integer idNum = idQuotient * 256 + idRemainder;
1081                             // found panel data - store it
1082                             panelColors.setPanelColor(idNum,
1083                                     HSBType.fromRGB(Integer.parseInt(panelDataPoints[i + 3]),
1084                                             Integer.parseInt(panelDataPoints[i + 4]),
1085                                             Integer.parseInt(panelDataPoints[i + 5])));
1086                         }
1087                     }
1088                 }
1089             } finally {
1090                 updateVisualLayout = true;
1091                 onPanelChangedColor();
1092             }
1093         }
1094     }
1095
1096     private @Nullable String getAddress() {
1097         return address;
1098     }
1099
1100     private void setAddress(String address) {
1101         this.address = address;
1102     }
1103
1104     private int getPort() {
1105         return port;
1106     }
1107
1108     private void setPort(int port) {
1109         this.port = port;
1110     }
1111
1112     private int getRefreshInterval() {
1113         return refreshIntervall;
1114     }
1115
1116     private void setRefreshIntervall(int refreshIntervall) {
1117         this.refreshIntervall = refreshIntervall;
1118     }
1119
1120     @Nullable
1121     private String getAuthToken() {
1122         return authToken;
1123     }
1124
1125     private void setAuthToken(@Nullable String authToken) {
1126         this.authToken = authToken;
1127     }
1128
1129     @Nullable
1130     private String getDeviceType() {
1131         return deviceType;
1132     }
1133
1134     private void setDeviceType(String deviceType) {
1135         this.deviceType = deviceType;
1136     }
1137
1138     private void stopAllJobs() {
1139         stopPairingJob();
1140         stopUpdateJob();
1141         stopTouchJob();
1142     }
1143 }