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.List;
22 import java.util.Objects;
23 import java.util.Scanner;
24 import java.util.concurrent.CopyOnWriteArrayList;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
30 import org.apache.commons.lang.StringUtils;
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.NanoleafInterruptedException;
44 import org.openhab.binding.nanoleaf.internal.NanoleafUnauthorizedException;
45 import org.openhab.binding.nanoleaf.internal.OpenAPIUtils;
46 import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
47 import org.openhab.binding.nanoleaf.internal.model.AuthToken;
48 import org.openhab.binding.nanoleaf.internal.model.BooleanState;
49 import org.openhab.binding.nanoleaf.internal.model.Brightness;
50 import org.openhab.binding.nanoleaf.internal.model.ControllerInfo;
51 import org.openhab.binding.nanoleaf.internal.model.Ct;
52 import org.openhab.binding.nanoleaf.internal.model.Effects;
53 import org.openhab.binding.nanoleaf.internal.model.Hue;
54 import org.openhab.binding.nanoleaf.internal.model.IntegerState;
55 import org.openhab.binding.nanoleaf.internal.model.Layout;
56 import org.openhab.binding.nanoleaf.internal.model.On;
57 import org.openhab.binding.nanoleaf.internal.model.Rhythm;
58 import org.openhab.binding.nanoleaf.internal.model.Sat;
59 import org.openhab.binding.nanoleaf.internal.model.State;
60 import org.openhab.binding.nanoleaf.internal.model.TouchEvents;
61 import org.openhab.core.config.core.Configuration;
62 import org.openhab.core.library.types.DecimalType;
63 import org.openhab.core.library.types.HSBType;
64 import org.openhab.core.library.types.IncreaseDecreaseType;
65 import org.openhab.core.library.types.OnOffType;
66 import org.openhab.core.library.types.PercentType;
67 import org.openhab.core.library.types.StringType;
68 import org.openhab.core.thing.Bridge;
69 import org.openhab.core.thing.ChannelUID;
70 import org.openhab.core.thing.Thing;
71 import org.openhab.core.thing.ThingStatus;
72 import org.openhab.core.thing.ThingStatusDetail;
73 import org.openhab.core.thing.binding.BaseBridgeHandler;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
79 import com.google.gson.Gson;
80 import com.google.gson.JsonSyntaxException;
83 * The {@link NanoleafControllerHandler} is responsible for handling commands to the controller which
84 * affect all panels connected to it (e.g. selected effect)
86 * @author Martin Raepple - Initial contribution
87 * @author Stefan Höhn - Canvas Touch Support
90 public class NanoleafControllerHandler extends BaseBridgeHandler {
92 // Pairing interval in seconds
93 private static final int PAIRING_INTERVAL = 25;
95 // Panel discovery interval in seconds
96 private static final int PANEL_DISCOVERY_INTERVAL = 30;
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<?> panelDiscoveryJob;
106 private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
108 // JSON parser for API responses
109 private final Gson gson = new Gson();
111 // Controller configuration settings and channel values
112 private @Nullable String address;
114 private int refreshIntervall;
115 private @Nullable String authToken;
116 private @Nullable String deviceType;
117 private @NonNullByDefault({}) ControllerInfo controllerInfo;
119 public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) {
121 this.httpClient = httpClient;
125 public void initialize() {
126 logger.debug("Initializing the controller (bridge)");
127 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED);
128 NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
129 setAddress(config.address);
130 setPort(config.port);
131 setRefreshIntervall(config.refreshInterval);
132 setAuthToken(config.authToken);
135 String property = getThing().getProperties().get(Thing.PROPERTY_MODEL_ID);
136 if (MODEL_ID_CANVAS.equals(property)) {
137 config.deviceType = DEVICE_TYPE_CANVAS;
139 config.deviceType = DEVICE_TYPE_LIGHTPANELS;
141 setDeviceType(config.deviceType);
144 Map<String, String> properties = getThing().getProperties();
145 if (StringUtils.isEmpty(getAddress()) || StringUtils.isEmpty(String.valueOf(getPort()))) {
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 (!StringUtils.isEmpty(properties.get(Thing.PROPERTY_FIRMWARE_VERSION))
151 && !OpenAPIUtils.checkRequiredFirmware(properties.get(Thing.PROPERTY_MODEL_ID),
152 properties.get(Thing.PROPERTY_FIRMWARE_VERSION))) {
153 logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
154 properties.get(Thing.PROPERTY_FIRMWARE_VERSION), API_MIN_FW_VER_LIGHTPANELS);
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
156 "@text/error.nanoleaf.controller.incompatibleFirmware");
158 } else if (StringUtils.isEmpty(getAuthToken())) {
159 logger.debug("No token found. Start pairing background job");
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
161 "@text/error.nanoleaf.controller.noToken");
164 stopPanelDiscoveryJob();
166 logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
167 updateStatus(ThingStatus.ONLINE);
170 startPanelDiscoveryJob();
173 } catch (IllegalArgumentException iae) {
174 logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
175 getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION));
176 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
177 "@text/error.nanoleaf.controller.incompatibleFirmware");
182 public void handleCommand(ChannelUID channelUID, Command command) {
183 logger.debug("Received command {} for channel {}", command, channelUID);
184 if (!ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) {
185 logger.debug("Cannot handle command. Bridge is not online.");
189 if (command instanceof RefreshType) {
190 updateFromControllerInfo();
192 switch (channelUID.getId()) {
195 case CHANNEL_COLOR_TEMPERATURE:
196 case CHANNEL_COLOR_TEMPERATURE_ABS:
197 case CHANNEL_PANEL_LAYOUT:
198 sendStateCommand(channelUID.getId(), command);
201 sendEffectCommand(command);
203 case CHANNEL_RHYTHM_MODE:
204 sendRhythmCommand(command);
207 logger.warn("Channel with id {} not handled", channelUID.getId());
211 } catch (NanoleafUnauthorizedException nae) {
212 logger.warn("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
214 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
215 "@text/error.nanoleaf.controller.invalidToken");
216 } catch (NanoleafException ne) {
217 logger.warn("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
218 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
219 "@text/error.nanoleaf.controller.communication");
224 public void handleRemoval() {
225 // delete token for openHAB
226 ContentResponse deleteTokenResponse;
228 Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER,
230 deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
231 if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
232 logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus());
235 logger.debug("Successfully deleted token for openHAB from controller");
236 } catch (NanoleafUnauthorizedException e) {
237 logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
238 } catch (NanoleafException ne) {
239 logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
242 super.handleRemoval();
243 logger.debug("Nanoleaf controller removed");
247 public void dispose() {
250 logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
253 public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
254 logger.debug("Register new listener for controller {}", getThing().getUID());
255 boolean result = controllerListeners.add(controllerListener);
257 startPanelDiscoveryJob();
262 public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
263 logger.debug("Unregister listener for controller {}", getThing().getUID());
264 boolean result = controllerListeners.remove(controllerListener);
266 stopPanelDiscoveryJob();
271 public NanoleafControllerConfig getControllerConfig() {
272 NanoleafControllerConfig config = new NanoleafControllerConfig();
273 config.address = this.getAddress();
274 config.port = this.getPort();
275 config.refreshInterval = this.getRefreshIntervall();
276 config.authToken = this.getAuthToken();
277 config.deviceType = this.getDeviceType();
281 public synchronized void startPairingJob() {
282 if (pairingJob == null || pairingJob.isCancelled()) {
283 logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
284 pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0, PAIRING_INTERVAL, TimeUnit.SECONDS);
288 private synchronized void stopPairingJob() {
289 if (pairingJob != null && !pairingJob.isCancelled()) {
290 logger.debug("Stop pairing job");
291 pairingJob.cancel(true);
292 this.pairingJob = null;
296 private synchronized void startUpdateJob() {
297 if (StringUtils.isNotEmpty(getAuthToken())) {
298 if (updateJob == null || updateJob.isCancelled()) {
299 logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall());
300 updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(),
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 public synchronized void startPanelDiscoveryJob() {
318 logger.debug("Starting panel discovery job. Has Controller-Listeners: {} panelDiscoveryJob: {}",
319 !controllerListeners.isEmpty(), panelDiscoveryJob);
320 if (!controllerListeners.isEmpty() && (panelDiscoveryJob == null || panelDiscoveryJob.isCancelled())) {
321 logger.debug("Start panel discovery job, interval={} sec", PANEL_DISCOVERY_INTERVAL);
322 panelDiscoveryJob = scheduler.scheduleWithFixedDelay(this::runPanelDiscovery, 0, PANEL_DISCOVERY_INTERVAL,
327 private synchronized void stopPanelDiscoveryJob() {
328 if (controllerListeners.isEmpty() && panelDiscoveryJob != null && !panelDiscoveryJob.isCancelled()) {
329 logger.debug("Stop panel discovery job");
330 panelDiscoveryJob.cancel(true);
331 this.panelDiscoveryJob = null;
335 private synchronized void startTouchJob() {
336 NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
337 if (!config.deviceType.equals(DEVICE_TYPE_CANVAS)) {
338 logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
339 this.getThing().getUID(), config.deviceType, DEVICE_TYPE_CANVAS);
342 logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
345 if (StringUtils.isNotEmpty(getAuthToken())) {
346 if (touchJob == null || touchJob.isCancelled()) {
347 logger.debug("Starting Touchjob now");
348 touchJob = scheduler.schedule(this::runTouchDetection, 0, TimeUnit.SECONDS);
351 logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID());
355 private synchronized void stopTouchJob() {
356 if (touchJob != null && !touchJob.isCancelled()) {
357 logger.debug("Stop touch job");
358 touchJob.cancel(true);
359 this.touchJob = null;
363 private void runUpdate() {
364 logger.debug("Run update job");
366 updateFromControllerInfo();
367 startTouchJob(); // if device type has changed, start touch detection.
368 // controller might have been offline, e.g. for firmware update. In this case, return to online state
369 if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
370 logger.debug("Controller {} is back online", thing.getUID());
371 updateStatus(ThingStatus.ONLINE);
373 } catch (NanoleafUnauthorizedException nae) {
374 logger.warn("Status update unauthorized: {}", nae.getMessage());
375 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
376 "@text/error.nanoleaf.controller.invalidToken");
377 if (StringUtils.isEmpty(getAuthToken())) {
378 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
379 "@text/error.nanoleaf.controller.noToken");
381 } catch (NanoleafException ne) {
382 logger.warn("Status update failed: {}", ne.getMessage());
383 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
384 "@text/error.nanoleaf.controller.communication");
385 } catch (RuntimeException e) {
386 logger.warn("Update job failed", e);
387 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
391 private void runPairing() {
392 logger.debug("Run pairing job");
394 if (StringUtils.isNotEmpty(getAuthToken())) {
395 if (pairingJob != null) {
396 pairingJob.cancel(false);
398 logger.debug("Authentication token found. Canceling pairing job");
401 ContentResponse authTokenResponse = OpenAPIUtils
402 .requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST).send();
403 if (logger.isTraceEnabled()) {
404 logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
407 if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
408 logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
409 authTokenResponse.getStatus());
411 // get auth token from response
413 AuthToken authToken = gson.fromJson(authTokenResponse.getContentAsString(), AuthToken.class);
415 if (StringUtils.isNotEmpty(authToken.getAuthToken())) {
416 logger.debug("Pairing succeeded.");
418 // Update and save the auth token in the thing configuration
419 Configuration config = editConfiguration();
420 config.put(NanoleafControllerConfig.AUTH_TOKEN, authToken.getAuthToken());
421 updateConfiguration(config);
423 updateStatus(ThingStatus.ONLINE);
424 // Update local field
425 setAuthToken(authToken.getAuthToken());
429 startPanelDiscoveryJob();
432 logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
433 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
434 "@text/error.nanoleaf.controller.pairingFailed");
435 throw new NanoleafException(authTokenResponse.getContentAsString());
438 } catch (JsonSyntaxException e) {
439 logger.warn("Received invalid data", e);
440 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
441 "@text/error.nanoleaf.controller.invalidData");
442 } catch (NanoleafException e) {
443 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
444 "@text/error.nanoleaf.controller.noTokenReceived");
445 } catch (InterruptedException | ExecutionException | TimeoutException e) {
446 logger.warn("Cannot send authorization request to controller: ", e);
447 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
448 "@text/error.nanoleaf.controller.authRequest");
449 } catch (RuntimeException e) {
450 logger.warn("Pairing job failed", e);
451 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
452 } catch (Exception e) {
453 logger.warn("Cannot start http client", e);
454 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
455 "@text/error.nanoleaf.controller.noClient");
459 private void runPanelDiscovery() {
460 logger.debug("Run panel discovery job");
461 // Trigger a new discovery of connected panels
462 for (NanoleafControllerListener controllerListener : controllerListeners) {
464 controllerListener.onControllerInfoFetched(getThing().getUID(), receiveControllerInfo());
465 } catch (NanoleafUnauthorizedException nue) {
466 logger.warn("Panel discovery unauthorized: {}", nue.getMessage());
467 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
468 "@text/error.nanoleaf.controller.invalidToken");
469 if (StringUtils.isEmpty(getAuthToken())) {
470 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
471 "@text/error.nanoleaf.controller.noToken");
473 } catch (NanoleafInterruptedException nie) {
474 logger.info("Panel discovery has been stopped.");
475 } catch (NanoleafException ne) {
476 logger.warn("Failed to discover panels: ", ne);
477 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
478 "@text/error.nanoleaf.controller.communication");
479 } catch (RuntimeException e) {
480 logger.warn("Panel discovery job failed", e);
481 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
487 * This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
489 private static boolean touchJobRunning = false;
491 private void runTouchDetection() {
492 if (touchJobRunning) {
493 logger.debug("touch job already running. quitting.");
497 touchJobRunning = true;
498 URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
499 logger.debug("touch job registered on: {}", eventUri.toString());
500 httpClient.newRequest(eventUri).send(new Response.Listener.Adapter() // request runs forever
503 public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
504 String s = StandardCharsets.UTF_8.decode(content).toString();
505 logger.trace("content {}", s);
507 Scanner eventContent = new Scanner(s);
508 while (eventContent.hasNextLine()) {
509 String line = eventContent.nextLine().trim();
510 // we don't expect anything than content id:4, so we do not check that but only care about the
512 if (line.startsWith("data:")) {
513 String json = line.substring(5).trim(); // supposed to be JSON
515 TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
516 handleTouchEvents(Objects.requireNonNull(touchEvents));
517 } catch (JsonSyntaxException jse) {
518 logger.error("couldn't parse touch event json {}", json);
522 eventContent.close();
523 logger.debug("leaving touch onContent");
524 super.onContent(response, content);
528 public void onSuccess(@Nullable Response response) {
529 logger.trace("touch event SUCCESS: {}", response);
533 public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
534 logger.trace("touch event FAILURE: {}", response);
538 public void onComplete(@Nullable Result result) {
539 logger.trace("touch event COMPLETE: {}", result);
542 } catch (RuntimeException | NanoleafException e) {
543 logger.warn("setting up TouchDetection failed", e);
545 touchJobRunning = false;
547 logger.debug("leaving run touch detection");
551 * Interate over all gathered touch events and apply them to the panel they belong to
555 private void handleTouchEvents(TouchEvents touchEvents) {
556 touchEvents.getEvents().forEach(event -> {
557 logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
559 // Iterate over all child things = all panels of that controller
560 this.getThing().getThings().forEach(child -> {
561 NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
562 if (panelHandler != null) {
563 logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
565 if (panelHandler.getPanelID().equals(event.getPanelId())) {
566 logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
568 panelHandler.updatePanelGesture(event.getGesture());
575 private void updateFromControllerInfo() throws NanoleafException {
576 logger.debug("Update channels for controller {}", thing.getUID());
577 this.controllerInfo = receiveControllerInfo();
578 if (controllerInfo == null) {
579 logger.debug("No Controller Info has been provided");
582 final State state = controllerInfo.getState();
584 OnOffType powerState = state.getOnOff();
585 updateState(CHANNEL_POWER, powerState);
588 Ct colorTemperature = state.getColorTemperature();
590 float colorTempPercent = 0f;
591 if (colorTemperature != null) {
592 updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new DecimalType(colorTemperature.getValue()));
595 Integer min = colorTemperature.getMin();
596 int colorMin = (min == null) ? 0 : min;
599 Integer max = colorTemperature.getMax();
600 int colorMax = (max == null) ? 0 : max;
602 colorTempPercent = (colorTemperature.getValue() - colorMin) / (colorMax - colorMin)
603 * PercentType.HUNDRED.intValue();
606 updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(Float.toString(colorTempPercent)));
607 updateState(CHANNEL_EFFECT, new StringType(controllerInfo.getEffects().getSelect()));
610 Hue stateHue = state.getHue();
611 int hue = (stateHue != null) ? stateHue.getValue() : 0;
613 Sat stateSaturation = state.getSaturation();
614 int saturation = (stateSaturation != null) ? stateSaturation.getValue() : 0;
616 Brightness stateBrightness = state.getBrightness();
617 int brightness = (stateBrightness != null) ? stateBrightness.getValue() : 0;
619 updateState(CHANNEL_COLOR, new HSBType(new DecimalType(hue), new PercentType(saturation),
620 new PercentType(powerState == OnOffType.ON ? brightness : 0)));
621 updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode()));
622 updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF);
623 updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode()));
624 updateState(CHANNEL_RHYTHM_STATE,
625 controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF);
626 // update bridge properties which may have changed, or are not present during discovery
627 Map<String, String> properties = editProperties();
628 properties.put(Thing.PROPERTY_SERIAL_NUMBER, controllerInfo.getSerialNo());
629 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, controllerInfo.getFirmwareVersion());
630 properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
631 properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
632 updateProperties(properties);
634 Configuration config = editConfiguration();
636 if (MODEL_ID_CANVAS.equals(controllerInfo.getModel())) {
637 config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_CANVAS);
638 logger.debug("Set to device type {}", DEVICE_TYPE_CANVAS);
640 config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_LIGHTPANELS);
641 logger.debug("Set to device type {}", DEVICE_TYPE_LIGHTPANELS);
643 updateConfiguration(config);
645 getConfig().getProperties().forEach((key, value) -> {
646 logger.trace("Configuration property: key {} value {}", key, value);
649 getThing().getProperties().forEach((key, value) -> {
650 logger.debug("Thing property: key {} value {}", key, value);
653 // update the color channels of each panel
654 this.getThing().getThings().forEach(child -> {
655 NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
656 if (panelHandler != null) {
657 logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
658 panelHandler.updatePanelColorChannel();
663 private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
664 ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient,
665 getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET));
666 ControllerInfo controllerInfo = gson.fromJson(controllerlInfoJSON.getContentAsString(), ControllerInfo.class);
667 return Objects.requireNonNull(controllerInfo);
670 private void sendStateCommand(String channel, Command command) throws NanoleafException {
671 State stateObject = new State();
674 if (command instanceof OnOffType) {
675 // On/Off command - turns controller on/off
676 BooleanState state = new On();
677 state.setValue(OnOffType.ON.equals(command));
678 stateObject.setState(state);
680 logger.warn("Unhandled command type: {}", command.getClass().getName());
685 if (command instanceof OnOffType) {
686 // On/Off command - turns controller on/off
687 BooleanState state = new On();
688 state.setValue(OnOffType.ON.equals(command));
689 stateObject.setState(state);
690 } else if (command instanceof HSBType) {
691 // regular color HSB command
692 IntegerState h = new Hue();
693 IntegerState s = new Sat();
694 IntegerState b = new Brightness();
695 h.setValue(((HSBType) command).getHue().intValue());
696 s.setValue(((HSBType) command).getSaturation().intValue());
697 b.setValue(((HSBType) command).getBrightness().intValue());
698 stateObject.setState(h);
699 stateObject.setState(s);
700 stateObject.setState(b);
701 } else if (command instanceof PercentType) {
702 // brightness command
703 IntegerState b = new Brightness();
704 b.setValue(((PercentType) command).intValue());
705 stateObject.setState(b);
706 } else if (command instanceof IncreaseDecreaseType) {
707 // increase/decrease brightness
708 if (controllerInfo != null) {
710 Brightness brightness = controllerInfo.getState().getBrightness();
711 int brightnessMin = 0;
712 int brightnessMax = 0;
713 if (brightness != null) {
715 Integer min = brightness.getMin();
716 brightnessMin = (min == null) ? 0 : min;
718 Integer max = brightness.getMax();
719 brightnessMax = (max == null) ? 0 : max;
721 if (IncreaseDecreaseType.INCREASE.equals(command)) {
723 Math.min(brightnessMax, brightness.getValue() + BRIGHTNESS_STEP_SIZE));
726 Math.max(brightnessMin, brightness.getValue() - BRIGHTNESS_STEP_SIZE));
728 stateObject.setState(brightness);
729 logger.debug("Setting controller brightness to {}", brightness.getValue());
730 // update controller info in case new command is sent before next update job interval
731 controllerInfo.getState().setBrightness(brightness);
733 logger.debug("Couldn't set brightness as it was null!");
737 logger.warn("Unhandled command type: {}", command.getClass().getName());
741 case CHANNEL_COLOR_TEMPERATURE:
742 if (command instanceof PercentType) {
743 // Color temperature (percent)
744 IntegerState state = new Ct();
746 Ct colorTemperature = controllerInfo.getState().getColorTemperature();
750 if (colorTemperature != null) {
752 Integer min = colorTemperature.getMin();
753 colorMin = (min == null) ? 0 : min;
756 Integer max = colorTemperature.getMax();
757 colorMax = (max == null) ? 0 : max;
760 state.setValue(Math.round((colorMax - colorMin) * (100 - ((PercentType) command).intValue())
761 / PercentType.HUNDRED.floatValue() + colorMin));
762 stateObject.setState(state);
764 logger.warn("Unhandled command type: {}", command.getClass().getName());
768 case CHANNEL_COLOR_TEMPERATURE_ABS:
769 if (command instanceof DecimalType) {
770 // Color temperature (absolute)
771 IntegerState state = new Ct();
772 state.setValue(((DecimalType) command).intValue());
773 stateObject.setState(state);
775 logger.warn("Unhandled command type: {}", command.getClass().getName());
779 case CHANNEL_PANEL_LAYOUT:
781 Layout layout = controllerInfo.getPanelLayout().getLayout();
782 String layoutView = (layout != null) ? layout.getLayoutView() : "";
783 logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
784 updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF);
787 logger.warn("Unhandled command type: {}", command.getClass().getName());
791 Request setNewStateRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_SET_VALUE,
793 setNewStateRequest.content(new StringContentProvider(gson.toJson(stateObject)), "application/json");
794 OpenAPIUtils.sendOpenAPIRequest(setNewStateRequest);
797 private void sendEffectCommand(Command command) throws NanoleafException {
798 Effects effects = new Effects();
799 if (command instanceof StringType) {
800 effects.setSelect(command.toString());
802 logger.warn("Unhandled command type: {}", command.getClass().getName());
805 Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
807 String content = gson.toJson(effects);
808 logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
809 setNewEffectRequest.content(new StringContentProvider(content), "application/json");
810 OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
813 private void sendRhythmCommand(Command command) throws NanoleafException {
814 Rhythm rhythm = new Rhythm();
815 if (command instanceof DecimalType) {
816 rhythm.setRhythmMode(((DecimalType) command).intValue());
818 logger.warn("Unhandled command type: {}", command.getClass().getName());
821 Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_RHYTHM_MODE,
823 setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
824 OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
827 private String getAddress() {
828 return StringUtils.defaultString(this.address);
831 private void setAddress(String address) {
832 this.address = address;
835 private int getPort() {
839 private void setPort(int port) {
843 private int getRefreshIntervall() {
844 return refreshIntervall;
847 private void setRefreshIntervall(int refreshIntervall) {
848 this.refreshIntervall = refreshIntervall;
851 private String getAuthToken() {
852 return StringUtils.defaultString(authToken);
855 private void setAuthToken(@Nullable String authToken) {
856 this.authToken = authToken;
859 private String getDeviceType() {
860 return StringUtils.defaultString(deviceType);
863 private void setDeviceType(String deviceType) {
864 this.deviceType = deviceType;
867 private void stopAllJobs() {
870 stopPanelDiscoveryJob();