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