2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nanoleaf.internal.handler;
15 import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.*;
18 import java.nio.ByteBuffer;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Collection;
21 import java.util.List;
23 import java.util.Objects;
24 import java.util.Scanner;
25 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.api.Response;
37 import org.eclipse.jetty.client.api.Result;
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.NanoleafControllerListener;
42 import org.openhab.binding.nanoleaf.internal.NanoleafException;
43 import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
44 import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
45 import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider;
46 import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
47 import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService;
48 import org.openhab.binding.nanoleaf.internal.model.AuthToken;
49 import org.openhab.binding.nanoleaf.internal.model.BooleanState;
50 import org.openhab.binding.nanoleaf.internal.model.Brightness;
51 import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
52 import org.openhab.binding.nanoleaf.internal.model.Ct;
53 import org.openhab.binding.nanoleaf.internal.model.Effects;
54 import org.openhab.binding.nanoleaf.internal.model.Hue;
55 import org.openhab.binding.nanoleaf.internal.model.IntegerState;
56 import org.openhab.binding.nanoleaf.internal.model.Layout;
57 import org.openhab.binding.nanoleaf.internal.model.On;
58 import org.openhab.binding.nanoleaf.internal.model.Rhythm;
59 import org.openhab.binding.nanoleaf.internal.model.Sat;
60 import org.openhab.binding.nanoleaf.internal.model.State;
61 import org.openhab.binding.nanoleaf.internal.model.TouchEvents;
62 import org.openhab.core.config.core.Configuration;
63 import org.openhab.core.library.types.DecimalType;
64 import org.openhab.core.library.types.HSBType;
65 import org.openhab.core.library.types.IncreaseDecreaseType;
66 import org.openhab.core.library.types.OnOffType;
67 import org.openhab.core.library.types.PercentType;
68 import org.openhab.core.library.types.StringType;
69 import org.openhab.core.thing.Bridge;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatus;
73 import org.openhab.core.thing.ThingStatusDetail;
74 import org.openhab.core.thing.binding.BaseBridgeHandler;
75 import org.openhab.core.thing.binding.ThingHandlerService;
76 import org.openhab.core.types.Command;
77 import org.openhab.core.types.RefreshType;
78 import org.slf4j.Logger;
79 import org.slf4j.LoggerFactory;
81 import com.google.gson.Gson;
82 import com.google.gson.JsonSyntaxException;
85 * The {@link NanoleafControllerHandler} is responsible for handling commands to the controller which
86 * affect all panels connected to it (e.g. selected effect)
88 * @author Martin Raepple - Initial contribution
89 * @author Stefan Höhn - Canvas Touch Support
90 * @author Kai Kreuzer - refactoring, bug fixing and code clean up
93 public class NanoleafControllerHandler extends BaseBridgeHandler {
95 // Pairing interval in seconds
96 private static final int PAIRING_INTERVAL = 10;
98 private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
99 private HttpClient httpClient;
100 private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<>();
102 // Pairing, update and panel discovery jobs and touch event job
103 private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
104 private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
105 private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
107 // JSON parser for API responses
108 private final Gson gson = new Gson();
110 // Controller configuration settings and channel values
111 private @Nullable String address;
113 private int refreshIntervall;
114 private @Nullable String authToken;
115 private @Nullable String deviceType;
116 private @NonNullByDefault({}) ControllerInfo controllerInfo;
118 public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) {
120 this.httpClient = httpClient;
124 public void initialize() {
125 logger.debug("Initializing the controller (bridge)");
126 updateStatus(ThingStatus.UNKNOWN);
127 NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
128 setAddress(config.address);
129 setPort(config.port);
130 setRefreshIntervall(config.refreshInterval);
131 setAuthToken(config.authToken);
133 Map<String, String> properties = getThing().getProperties();
134 String propertyModelId = properties.get(Thing.PROPERTY_MODEL_ID);
135 if (hasTouchSupport(propertyModelId)) {
136 config.deviceType = DEVICE_TYPE_TOUCHSUPPORT;
138 config.deviceType = DEVICE_TYPE_LIGHTPANELS;
140 setDeviceType(config.deviceType);
142 String propertyFirmwareVersion = properties.get(Thing.PROPERTY_FIRMWARE_VERSION);
145 if (config.address.isEmpty() || String.valueOf(config.port).isEmpty()) {
146 logger.warn("No IP address and port configured for the Nanoleaf controller");
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
148 "@text/error.nanoleaf.controller.noIp");
150 } else if (propertyFirmwareVersion != null && !propertyFirmwareVersion.isEmpty() && !OpenAPIUtils
151 .checkRequiredFirmware(properties.get(Thing.PROPERTY_MODEL_ID), propertyFirmwareVersion)) {
152 logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
153 propertyFirmwareVersion, API_MIN_FW_VER_LIGHTPANELS);
154 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
155 "@text/error.nanoleaf.controller.incompatibleFirmware");
157 } else if (config.authToken == null || config.authToken.isEmpty()) {
158 logger.debug("No token found. Start pairing background job");
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
160 "@text/error.nanoleaf.controller.noToken");
168 } catch (IllegalArgumentException iae) {
169 logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
170 getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION));
171 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
172 "@text/error.nanoleaf.controller.incompatibleFirmware");
177 public void handleCommand(ChannelUID channelUID, Command command) {
178 logger.debug("Received command {} for channel {}", command, channelUID);
179 if (!ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) {
180 logger.debug("Cannot handle command. Bridge is not online.");
184 if (command instanceof RefreshType) {
185 updateFromControllerInfo();
187 switch (channelUID.getId()) {
189 case CHANNEL_COLOR_TEMPERATURE:
190 case CHANNEL_COLOR_TEMPERATURE_ABS:
191 sendStateCommand(channelUID.getId(), command);
194 sendEffectCommand(command);
196 case CHANNEL_RHYTHM_MODE:
197 sendRhythmCommand(command);
200 logger.warn("Channel with id {} not handled", channelUID.getId());
204 } catch (NanoleafUnauthorizedException nae) {
205 logger.warn("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
208 "@text/error.nanoleaf.controller.invalidToken");
209 } catch (NanoleafException ne) {
210 logger.warn("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
211 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
212 "@text/error.nanoleaf.controller.communication");
217 public void handleRemoval() {
218 scheduler.execute(() -> {
219 // delete token for openHAB
220 ContentResponse deleteTokenResponse;
222 Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(),
223 API_DELETE_USER, HttpMethod.DELETE);
224 deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
225 if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
226 logger.warn("Failed to delete token for openHAB. Response code is {}",
227 deleteTokenResponse.getStatus());
230 logger.debug("Successfully deleted token for openHAB from controller");
231 } catch (NanoleafUnauthorizedException e) {
232 logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
233 } catch (NanoleafException ne) {
234 logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
237 super.handleRemoval();
238 logger.debug("Nanoleaf controller removed");
243 public void dispose() {
246 logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
250 public Collection<Class<? extends ThingHandlerService>> getServices() {
251 return List.of(NanoleafPanelsDiscoveryService.class, NanoleafCommandDescriptionProvider.class);
254 public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
255 logger.debug("Register new listener for controller {}", getThing().getUID());
256 return controllerListeners.add(controllerListener);
259 public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
260 logger.debug("Unregister listener for controller {}", getThing().getUID());
261 return controllerListeners.remove(controllerListener);
264 public NanoleafControllerConfig getControllerConfig() {
265 NanoleafControllerConfig config = new NanoleafControllerConfig();
266 config.address = Objects.requireNonNullElse(getAddress(), "");
267 config.port = getPort();
268 config.refreshInterval = getRefreshInterval();
269 config.authToken = getAuthToken();
270 config.deviceType = Objects.requireNonNullElse(getDeviceType(), "");
274 public String getLayout() {
275 Layout layout = controllerInfo.getPanelLayout().getLayout();
276 String layoutView = (layout != null) ? layout.getLayoutView() : "";
280 public synchronized void startPairingJob() {
281 if (pairingJob == null || pairingJob.isCancelled()) {
282 logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
283 pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0, PAIRING_INTERVAL, TimeUnit.SECONDS);
287 private synchronized void stopPairingJob() {
288 if (pairingJob != null && !pairingJob.isCancelled()) {
289 logger.debug("Stop pairing job");
290 pairingJob.cancel(true);
291 this.pairingJob = null;
295 private synchronized void startUpdateJob() {
296 String localAuthToken = getAuthToken();
297 if (localAuthToken != null && !localAuthToken.isEmpty()) {
298 if (updateJob == null || updateJob.isCancelled()) {
299 logger.debug("Start controller status job, repeat every {} sec", getRefreshInterval());
300 updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshInterval(),
304 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
305 "@text/error.nanoleaf.controller.noToken");
309 private synchronized void stopUpdateJob() {
310 if (updateJob != null && !updateJob.isCancelled()) {
311 logger.debug("Stop status job");
312 updateJob.cancel(true);
313 this.updateJob = null;
317 private synchronized void startTouchJob() {
318 NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
319 if (!config.deviceType.equals(DEVICE_TYPE_TOUCHSUPPORT)) {
320 logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
321 this.getThing().getUID(), config.deviceType, DEVICE_TYPE_TOUCHSUPPORT);
324 logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
327 String localAuthToken = getAuthToken();
328 if (localAuthToken != null && !localAuthToken.isEmpty()) {
329 if (touchJob == null || touchJob.isCancelled()) {
330 logger.debug("Starting Touchjob now");
331 touchJob = scheduler.schedule(this::runTouchDetection, 0, TimeUnit.SECONDS);
334 logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID());
338 private boolean hasTouchSupport(@Nullable String deviceType) {
339 return (MODELS_WITH_TOUCHSUPPORT.contains(deviceType));
342 private synchronized void stopTouchJob() {
343 if (touchJob != null && !touchJob.isCancelled()) {
344 logger.debug("Stop touch job");
345 touchJob.cancel(true);
346 this.touchJob = null;
350 private void runUpdate() {
351 logger.debug("Run update job");
353 updateFromControllerInfo();
354 startTouchJob(); // if device type has changed, start touch detection.
355 updateStatus(ThingStatus.ONLINE);
356 } catch (NanoleafUnauthorizedException nae) {
357 logger.warn("Status update unauthorized: {}", nae.getMessage());
358 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
359 "@text/error.nanoleaf.controller.invalidToken");
360 String localAuthToken = getAuthToken();
361 if (localAuthToken == null || localAuthToken.isEmpty()) {
362 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
363 "@text/error.nanoleaf.controller.noToken");
365 } catch (NanoleafException ne) {
366 logger.warn("Status update failed: {}", ne.getMessage());
367 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
368 "@text/error.nanoleaf.controller.communication");
369 } catch (RuntimeException e) {
370 logger.warn("Update job failed", e);
371 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
375 private void runPairing() {
376 logger.debug("Run pairing job");
378 String localAuthToken = getAuthToken();
379 if (localAuthToken != null && !localAuthToken.isEmpty()) {
380 if (pairingJob != null) {
381 pairingJob.cancel(false);
383 logger.debug("Authentication token found. Canceling pairing job");
386 ContentResponse authTokenResponse = OpenAPIUtils
387 .requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST)
388 .timeout(20, TimeUnit.SECONDS).send();
389 if (logger.isTraceEnabled()) {
390 logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
393 if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
394 logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
395 authTokenResponse.getStatus());
397 // get auth token from response
398 AuthToken authTokenObject = gson.fromJson(authTokenResponse.getContentAsString(), AuthToken.class);
399 localAuthToken = authTokenObject.getAuthToken();
400 if (localAuthToken != null && !localAuthToken.isEmpty()) {
401 logger.debug("Pairing succeeded.");
403 // Update and save the auth token in the thing configuration
404 Configuration config = editConfiguration();
405 config.put(NanoleafControllerConfig.AUTH_TOKEN, localAuthToken);
406 updateConfiguration(config);
408 updateStatus(ThingStatus.ONLINE);
409 // Update local field
410 setAuthToken(localAuthToken);
416 logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
417 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
418 "@text/error.nanoleaf.controller.pairingFailed");
419 throw new NanoleafException(authTokenResponse.getContentAsString());
422 } catch (JsonSyntaxException e) {
423 logger.warn("Received invalid data", e);
424 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
425 "@text/error.nanoleaf.controller.invalidData");
426 } catch (NanoleafException e) {
427 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
428 "@text/error.nanoleaf.controller.noTokenReceived");
429 } catch (InterruptedException | ExecutionException | TimeoutException e) {
430 logger.debug("Cannot send authorization request to controller: ", e);
431 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
432 "@text/error.nanoleaf.controller.authRequest");
433 } catch (RuntimeException e) {
434 logger.warn("Pairing job failed", e);
435 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
436 } catch (Exception e) {
437 logger.warn("Cannot start http client", e);
438 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
439 "@text/error.nanoleaf.controller.noClient");
444 * This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
446 private static boolean touchJobRunning = false;
448 private void runTouchDetection() {
449 if (touchJobRunning) {
450 logger.debug("touch job already running. quitting.");
454 touchJobRunning = true;
455 URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
456 logger.debug("touch job registered on: {}", eventUri.toString());
457 httpClient.newRequest(eventUri).send(new Response.Listener.Adapter() // request runs forever
460 public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
461 String s = StandardCharsets.UTF_8.decode(content).toString();
462 logger.trace("content {}", s);
464 Scanner eventContent = new Scanner(s);
465 while (eventContent.hasNextLine()) {
466 String line = eventContent.nextLine().trim();
467 // we don't expect anything than content id:4, so we do not check that but only care about the
469 if (line.startsWith("data:")) {
470 String json = line.substring(5).trim(); // supposed to be JSON
472 TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
473 handleTouchEvents(Objects.requireNonNull(touchEvents));
474 } catch (JsonSyntaxException jse) {
475 logger.error("couldn't parse touch event json {}", json);
479 eventContent.close();
480 logger.debug("leaving touch onContent");
481 super.onContent(response, content);
485 public void onSuccess(@Nullable Response response) {
486 logger.trace("touch event SUCCESS: {}", response);
490 public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
491 logger.trace("touch event FAILURE: {}", response);
495 public void onComplete(@Nullable Result result) {
496 logger.trace("touch event COMPLETE: {}", result);
499 } catch (RuntimeException | NanoleafException e) {
500 logger.warn("setting up TouchDetection failed", e);
502 touchJobRunning = false;
504 logger.debug("leaving run touch detection");
508 * Interate over all gathered touch events and apply them to the panel they belong to
512 private void handleTouchEvents(TouchEvents touchEvents) {
513 touchEvents.getEvents().forEach(event -> {
514 logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
516 // Iterate over all child things = all panels of that controller
517 this.getThing().getThings().forEach(child -> {
518 NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
519 if (panelHandler != null) {
520 logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
522 if (panelHandler.getPanelID().equals(event.getPanelId())) {
523 logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
525 panelHandler.updatePanelGesture(event.getGesture());
532 private void updateFromControllerInfo() throws NanoleafException {
533 logger.debug("Update channels for controller {}", thing.getUID());
534 this.controllerInfo = receiveControllerInfo();
535 final State state = controllerInfo.getState();
537 OnOffType powerState = state.getOnOff();
540 Ct colorTemperature = state.getColorTemperature();
542 float colorTempPercent = 0f;
543 if (colorTemperature != null) {
544 updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new DecimalType(colorTemperature.getValue()));
547 Integer min = colorTemperature.getMin();
548 int colorMin = (min == null) ? 0 : min;
551 Integer max = colorTemperature.getMax();
552 int colorMax = (max == null) ? 0 : max;
554 colorTempPercent = (colorTemperature.getValue() - colorMin) / (colorMax - colorMin)
555 * PercentType.HUNDRED.intValue();
558 updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(Float.toString(colorTempPercent)));
559 updateState(CHANNEL_EFFECT, new StringType(controllerInfo.getEffects().getSelect()));
562 Hue stateHue = state.getHue();
563 int hue = (stateHue != null) ? stateHue.getValue() : 0;
565 Sat stateSaturation = state.getSaturation();
566 int saturation = (stateSaturation != null) ? stateSaturation.getValue() : 0;
568 Brightness stateBrightness = state.getBrightness();
569 int brightness = (stateBrightness != null) ? stateBrightness.getValue() : 0;
571 updateState(CHANNEL_COLOR, new HSBType(new DecimalType(hue), new PercentType(saturation),
572 new PercentType(powerState == OnOffType.ON ? brightness : 0)));
573 updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode()));
574 updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF);
575 updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode()));
576 updateState(CHANNEL_RHYTHM_STATE,
577 controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF);
578 // update bridge properties which may have changed, or are not present during discovery
579 Map<String, String> properties = editProperties();
580 properties.put(Thing.PROPERTY_SERIAL_NUMBER, controllerInfo.getSerialNo());
581 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, controllerInfo.getFirmwareVersion());
582 properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
583 properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
584 updateProperties(properties);
586 Configuration config = editConfiguration();
588 if (hasTouchSupport(controllerInfo.getModel())) {
589 config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_TOUCHSUPPORT);
590 logger.debug("Set to device type {}", DEVICE_TYPE_TOUCHSUPPORT);
592 config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_LIGHTPANELS);
593 logger.debug("Set to device type {}", DEVICE_TYPE_LIGHTPANELS);
595 updateConfiguration(config);
597 getConfig().getProperties().forEach((key, value) -> {
598 logger.trace("Configuration property: key {} value {}", key, value);
601 getThing().getProperties().forEach((key, value) -> {
602 logger.debug("Thing property: key {} value {}", key, value);
605 // update the color channels of each panel
606 this.getThing().getThings().forEach(child -> {
607 NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
608 if (panelHandler != null) {
609 logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
610 panelHandler.updatePanelColorChannel();
614 for (NanoleafControllerListener controllerListener : controllerListeners) {
615 controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo);
619 private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
620 ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient,
621 getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET));
622 ControllerInfo controllerInfo = gson.fromJson(controllerlInfoJSON.getContentAsString(), ControllerInfo.class);
623 return Objects.requireNonNull(controllerInfo);
626 private void sendStateCommand(String channel, Command command) throws NanoleafException {
627 State stateObject = new State();
630 if (command instanceof OnOffType) {
631 // On/Off command - turns controller on/off
632 BooleanState state = new On();
633 state.setValue(OnOffType.ON.equals(command));
634 stateObject.setState(state);
635 } else if (command instanceof HSBType) {
636 // regular color HSB command
637 IntegerState h = new Hue();
638 IntegerState s = new Sat();
639 IntegerState b = new Brightness();
640 h.setValue(((HSBType) command).getHue().intValue());
641 s.setValue(((HSBType) command).getSaturation().intValue());
642 b.setValue(((HSBType) command).getBrightness().intValue());
643 stateObject.setState(h);
644 stateObject.setState(s);
645 stateObject.setState(b);
646 } else if (command instanceof PercentType) {
647 // brightness command
648 IntegerState b = new Brightness();
649 b.setValue(((PercentType) command).intValue());
650 stateObject.setState(b);
651 } else if (command instanceof IncreaseDecreaseType) {
652 // increase/decrease brightness
653 if (controllerInfo != null) {
655 Brightness brightness = controllerInfo.getState().getBrightness();
656 int brightnessMin = 0;
657 int brightnessMax = 0;
658 if (brightness != null) {
660 Integer min = brightness.getMin();
661 brightnessMin = (min == null) ? 0 : min;
663 Integer max = brightness.getMax();
664 brightnessMax = (max == null) ? 0 : max;
666 if (IncreaseDecreaseType.INCREASE.equals(command)) {
668 Math.min(brightnessMax, brightness.getValue() + BRIGHTNESS_STEP_SIZE));
671 Math.max(brightnessMin, brightness.getValue() - BRIGHTNESS_STEP_SIZE));
673 stateObject.setState(brightness);
674 logger.debug("Setting controller brightness to {}", brightness.getValue());
675 // update controller info in case new command is sent before next update job interval
676 controllerInfo.getState().setBrightness(brightness);
678 logger.debug("Couldn't set brightness as it was null!");
682 logger.warn("Unhandled command type: {}", command.getClass().getName());
686 case CHANNEL_COLOR_TEMPERATURE:
687 if (command instanceof PercentType) {
688 // Color temperature (percent)
689 IntegerState state = new Ct();
691 Ct colorTemperature = controllerInfo.getState().getColorTemperature();
695 if (colorTemperature != null) {
697 Integer min = colorTemperature.getMin();
698 colorMin = (min == null) ? 0 : min;
701 Integer max = colorTemperature.getMax();
702 colorMax = (max == null) ? 0 : max;
705 state.setValue(Math.round((colorMax - colorMin) * (100 - ((PercentType) command).intValue())
706 / PercentType.HUNDRED.floatValue() + colorMin));
707 stateObject.setState(state);
709 logger.warn("Unhandled command type: {}", command.getClass().getName());
713 case CHANNEL_COLOR_TEMPERATURE_ABS:
714 if (command instanceof DecimalType) {
715 // Color temperature (absolute)
716 IntegerState state = new Ct();
717 state.setValue(((DecimalType) command).intValue());
718 stateObject.setState(state);
720 logger.warn("Unhandled command type: {}", command.getClass().getName());
725 logger.warn("Unhandled command type: {}", command.getClass().getName());
729 Request setNewStateRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_SET_VALUE,
731 setNewStateRequest.content(new StringContentProvider(gson.toJson(stateObject)), "application/json");
732 OpenAPIUtils.sendOpenAPIRequest(setNewStateRequest);
735 private void sendEffectCommand(Command command) throws NanoleafException {
736 Effects effects = new Effects();
737 if (command instanceof StringType) {
738 effects.setSelect(command.toString());
740 logger.warn("Unhandled command type: {}", command.getClass().getName());
743 Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
745 String content = gson.toJson(effects);
746 logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
747 setNewEffectRequest.content(new StringContentProvider(content), "application/json");
748 OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
751 private void sendRhythmCommand(Command command) throws NanoleafException {
752 Rhythm rhythm = new Rhythm();
753 if (command instanceof DecimalType) {
754 rhythm.setRhythmMode(((DecimalType) command).intValue());
756 logger.warn("Unhandled command type: {}", command.getClass().getName());
759 Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_RHYTHM_MODE,
761 setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
762 OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
765 private @Nullable String getAddress() {
769 private void setAddress(String address) {
770 this.address = address;
773 private int getPort() {
777 private void setPort(int port) {
781 private int getRefreshInterval() {
782 return refreshIntervall;
785 private void setRefreshIntervall(int refreshIntervall) {
786 this.refreshIntervall = refreshIntervall;
789 private @Nullable String getAuthToken() {
793 private void setAuthToken(@Nullable String authToken) {
794 this.authToken = authToken;
797 private @Nullable String getDeviceType() {
801 private void setDeviceType(String deviceType) {
802 this.deviceType = deviceType;
805 private void stopAllJobs() {