2 * Copyright (c) 2010-2020 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.Scanner;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
29 import org.apache.commons.lang.StringUtils;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.client.api.Request;
35 import org.eclipse.jetty.client.api.Response;
36 import org.eclipse.jetty.client.api.Result;
37 import org.eclipse.jetty.client.util.StringContentProvider;
38 import org.eclipse.jetty.http.HttpMethod;
39 import org.eclipse.jetty.http.HttpStatus;
40 import org.openhab.binding.nanoleaf.internal.*;
41 import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig;
42 import org.openhab.binding.nanoleaf.internal.model.*;
43 import org.openhab.core.config.core.Configuration;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.HSBType;
46 import org.openhab.core.library.types.IncreaseDecreaseType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.PercentType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.BaseBridgeHandler;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
61 import com.google.gson.Gson;
62 import com.google.gson.JsonSyntaxException;
65 * The {@link NanoleafControllerHandler} is responsible for handling commands to the controller which
66 * affect all panels connected to it (e.g. selected effect)
68 * @author Martin Raepple - Initial contribution
69 * @author Stefan Höhn - Canvas Touch Support
72 public class NanoleafControllerHandler extends BaseBridgeHandler {
74 // Pairing interval in seconds
75 private static final int PAIRING_INTERVAL = 25;
77 // Panel discovery interval in seconds
78 private static final int PANEL_DISCOVERY_INTERVAL = 30;
80 private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class);
81 private HttpClient httpClient;
82 private List<NanoleafControllerListener> controllerListeners = new CopyOnWriteArrayList<>();
84 // Pairing, update and panel discovery jobs and touch event job
85 private @NonNullByDefault({}) ScheduledFuture<?> pairingJob;
86 private @NonNullByDefault({}) ScheduledFuture<?> updateJob;
87 private @NonNullByDefault({}) ScheduledFuture<?> panelDiscoveryJob;
88 private @NonNullByDefault({}) ScheduledFuture<?> touchJob;
90 // JSON parser for API responses
91 private final Gson gson = new Gson();
93 // Controller configuration settings and channel values
94 private @Nullable String address;
96 private int refreshIntervall;
97 private @Nullable String authToken;
98 private @Nullable String deviceType;
99 private @NonNullByDefault({}) ControllerInfo controllerInfo;
101 public NanoleafControllerHandler(Bridge bridge, HttpClient httpClient) {
103 this.httpClient = httpClient;
107 public void initialize() {
108 logger.debug("Initializing the controller (bridge)");
109 updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED);
110 NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
111 setAddress(config.address);
112 setPort(config.port);
113 setRefreshIntervall(config.refreshInterval);
114 setAuthToken(config.authToken);
117 String property = getThing().getProperties().get(Thing.PROPERTY_MODEL_ID);
118 if (MODEL_ID_CANVAS.equals(property)) {
119 config.deviceType = DEVICE_TYPE_CANVAS;
121 config.deviceType = DEVICE_TYPE_LIGHTPANELS;
123 setDeviceType(config.deviceType);
126 Map<String, String> properties = getThing().getProperties();
127 if (StringUtils.isEmpty(getAddress()) || StringUtils.isEmpty(String.valueOf(getPort()))) {
128 logger.warn("No IP address and port configured for the Nanoleaf controller");
129 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
130 "@text/error.nanoleaf.controller.noIp");
132 } else if (!StringUtils.isEmpty(properties.get(Thing.PROPERTY_FIRMWARE_VERSION))
133 && !OpenAPIUtils.checkRequiredFirmware(properties.get(Thing.PROPERTY_MODEL_ID),
134 properties.get(Thing.PROPERTY_FIRMWARE_VERSION))) {
135 logger.warn("Nanoleaf controller firmware is too old: {}. Must be equal or higher than {}",
136 properties.get(Thing.PROPERTY_FIRMWARE_VERSION), API_MIN_FW_VER_LIGHTPANELS);
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
138 "@text/error.nanoleaf.controller.incompatibleFirmware");
140 } else if (StringUtils.isEmpty(getAuthToken())) {
141 logger.debug("No token found. Start pairing background job");
142 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
143 "@text/error.nanoleaf.controller.noToken");
146 stopPanelDiscoveryJob();
148 logger.debug("Controller is online. Stop pairing job, start update & panel discovery jobs");
149 updateStatus(ThingStatus.ONLINE);
152 startPanelDiscoveryJob();
155 } catch (IllegalArgumentException iae) {
156 logger.warn("Nanoleaf controller firmware version not in format x.y.z: {}",
157 getThing().getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION));
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
159 "@text/error.nanoleaf.controller.incompatibleFirmware");
164 public void handleCommand(ChannelUID channelUID, Command command) {
165 logger.debug("Received command {} for channel {}", command, channelUID);
166 if (!ThingStatus.ONLINE.equals(getThing().getStatusInfo().getStatus())) {
167 logger.debug("Cannot handle command. Bridge is not online.");
171 if (command instanceof RefreshType) {
172 updateFromControllerInfo();
174 switch (channelUID.getId()) {
177 case CHANNEL_COLOR_TEMPERATURE:
178 case CHANNEL_COLOR_TEMPERATURE_ABS:
179 case CHANNEL_PANEL_LAYOUT:
180 sendStateCommand(channelUID.getId(), command);
183 sendEffectCommand(command);
185 case CHANNEL_RHYTHM_MODE:
186 sendRhythmCommand(command);
189 logger.warn("Channel with id {} not handled", channelUID.getId());
193 } catch (NanoleafUnauthorizedException nae) {
194 logger.warn("Authorization for command {} to channelUID {} failed: {}", command, channelUID,
196 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
197 "@text/error.nanoleaf.controller.invalidToken");
198 } catch (NanoleafException ne) {
199 logger.warn("Handling command {} to channelUID {} failed: {}", command, channelUID, ne.getMessage());
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
201 "@text/error.nanoleaf.controller.communication");
206 public void handleRemoval() {
207 // delete token for openHAB
208 ContentResponse deleteTokenResponse;
210 Request deleteTokenRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_DELETE_USER,
212 deleteTokenResponse = OpenAPIUtils.sendOpenAPIRequest(deleteTokenRequest);
213 if (deleteTokenResponse.getStatus() != HttpStatus.NO_CONTENT_204) {
214 logger.warn("Failed to delete token for openHAB. Response code is {}", deleteTokenResponse.getStatus());
217 logger.debug("Successfully deleted token for openHAB from controller");
218 } catch (NanoleafUnauthorizedException e) {
219 logger.warn("Attempt to delete token for openHAB failed. Token unauthorized.");
220 } catch (NanoleafException ne) {
221 logger.warn("Attempt to delete token for openHAB failed : {}", ne.getMessage());
224 super.handleRemoval();
225 logger.debug("Nanoleaf controller removed");
229 public void dispose() {
232 logger.debug("Disposing handler for Nanoleaf controller {}", getThing().getUID());
235 public boolean registerControllerListener(NanoleafControllerListener controllerListener) {
236 logger.debug("Register new listener for controller {}", getThing().getUID());
237 boolean result = controllerListeners.add(controllerListener);
239 startPanelDiscoveryJob();
244 public boolean unregisterControllerListener(NanoleafControllerListener controllerListener) {
245 logger.debug("Unregister listener for controller {}", getThing().getUID());
246 boolean result = controllerListeners.remove(controllerListener);
248 stopPanelDiscoveryJob();
253 public NanoleafControllerConfig getControllerConfig() {
254 NanoleafControllerConfig config = new NanoleafControllerConfig();
255 config.address = this.getAddress();
256 config.port = this.getPort();
257 config.refreshInterval = this.getRefreshIntervall();
258 config.authToken = this.getAuthToken();
259 config.deviceType = this.getDeviceType();
263 public synchronized void startPairingJob() {
264 if (pairingJob == null || pairingJob.isCancelled()) {
265 logger.debug("Start pairing job, interval={} sec", PAIRING_INTERVAL);
266 pairingJob = scheduler.scheduleWithFixedDelay(this::runPairing, 0, PAIRING_INTERVAL, TimeUnit.SECONDS);
270 private synchronized void stopPairingJob() {
271 if (pairingJob != null && !pairingJob.isCancelled()) {
272 logger.debug("Stop pairing job");
273 pairingJob.cancel(true);
274 this.pairingJob = null;
278 private synchronized void startUpdateJob() {
279 if (StringUtils.isNotEmpty(getAuthToken())) {
280 if (updateJob == null || updateJob.isCancelled()) {
281 logger.debug("Start controller status job, repeat every {} sec", getRefreshIntervall());
282 updateJob = scheduler.scheduleWithFixedDelay(this::runUpdate, 0, getRefreshIntervall(),
286 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
287 "@text/error.nanoleaf.controller.noToken");
291 private synchronized void stopUpdateJob() {
292 if (updateJob != null && !updateJob.isCancelled()) {
293 logger.debug("Stop status job");
294 updateJob.cancel(true);
295 this.updateJob = null;
299 public synchronized void startPanelDiscoveryJob() {
300 logger.debug("Starting panel discovery job. Has Controller-Listeners: {} panelDiscoveryJob: {}",
301 !controllerListeners.isEmpty(), panelDiscoveryJob);
302 if (!controllerListeners.isEmpty() && (panelDiscoveryJob == null || panelDiscoveryJob.isCancelled())) {
303 logger.debug("Start panel discovery job, interval={} sec", PANEL_DISCOVERY_INTERVAL);
304 panelDiscoveryJob = scheduler.scheduleWithFixedDelay(this::runPanelDiscovery, 0, PANEL_DISCOVERY_INTERVAL,
309 private synchronized void stopPanelDiscoveryJob() {
310 if (controllerListeners.isEmpty() && panelDiscoveryJob != null && !panelDiscoveryJob.isCancelled()) {
311 logger.debug("Stop panel discovery job");
312 panelDiscoveryJob.cancel(true);
313 this.panelDiscoveryJob = null;
317 private synchronized void startTouchJob() {
318 NanoleafControllerConfig config = getConfigAs(NanoleafControllerConfig.class);
319 if (!config.deviceType.equals(DEVICE_TYPE_CANVAS)) {
320 logger.debug("NOT starting TouchJob for Panel {} because it has wrong device type '{}' vs required '{}'",
321 this.getThing().getUID(), config.deviceType, DEVICE_TYPE_CANVAS);
324 logger.debug("Starting TouchJob for Panel {}", this.getThing().getUID());
326 if (StringUtils.isNotEmpty(getAuthToken())) {
327 if (touchJob == null || touchJob.isCancelled()) {
328 logger.debug("Starting Touchjob now");
329 touchJob = scheduler.schedule(this::runTouchDetection, 0, TimeUnit.SECONDS);
332 logger.error("starting TouchJob for Controller {} failed - missing token", this.getThing().getUID());
336 private synchronized void stopTouchJob() {
337 if (touchJob != null && !touchJob.isCancelled()) {
338 logger.debug("Stop touch job");
339 touchJob.cancel(true);
340 this.touchJob = null;
344 private void runUpdate() {
345 logger.debug("Run update job");
347 updateFromControllerInfo();
348 startTouchJob(); // if device type has changed, start touch detection.
349 // controller might have been offline, e.g. for firmware update. In this case, return to online state
350 if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
351 logger.debug("Controller {} is back online", thing.getUID());
352 updateStatus(ThingStatus.ONLINE);
354 } catch (NanoleafUnauthorizedException nae) {
355 logger.warn("Status update unauthorized: {}", nae.getMessage());
356 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
357 "@text/error.nanoleaf.controller.invalidToken");
358 if (StringUtils.isEmpty(getAuthToken())) {
359 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
360 "@text/error.nanoleaf.controller.noToken");
362 } catch (NanoleafException ne) {
363 logger.warn("Status update failed: {}", ne.getMessage());
364 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
365 "@text/error.nanoleaf.controller.communication");
366 } catch (RuntimeException e) {
367 logger.warn("Update job failed", e);
368 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
372 private void runPairing() {
373 logger.debug("Run pairing job");
375 if (StringUtils.isNotEmpty(getAuthToken())) {
376 if (pairingJob != null) {
377 pairingJob.cancel(false);
379 logger.debug("Authentication token found. Canceling pairing job");
382 ContentResponse authTokenResponse = OpenAPIUtils
383 .requestBuilder(httpClient, getControllerConfig(), API_ADD_USER, HttpMethod.POST).send();
384 if (logger.isTraceEnabled()) {
385 logger.trace("Auth token response: {}", authTokenResponse.getContentAsString());
388 if (authTokenResponse.getStatus() != HttpStatus.OK_200) {
389 logger.debug("Pairing pending for {}. Controller returns status code {}", this.getThing().getUID(),
390 authTokenResponse.getStatus());
392 // get auth token from response
394 AuthToken authToken = gson.fromJson(authTokenResponse.getContentAsString(), AuthToken.class);
396 if (StringUtils.isNotEmpty(authToken.getAuthToken())) {
397 logger.debug("Pairing succeeded.");
399 // Update and save the auth token in the thing configuration
400 Configuration config = editConfiguration();
401 config.put(NanoleafControllerConfig.AUTH_TOKEN, authToken.getAuthToken());
402 updateConfiguration(config);
404 updateStatus(ThingStatus.ONLINE);
405 // Update local field
406 setAuthToken(authToken.getAuthToken());
410 startPanelDiscoveryJob();
413 logger.debug("No auth token found in response: {}", authTokenResponse.getContentAsString());
414 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
415 "@text/error.nanoleaf.controller.pairingFailed");
416 throw new NanoleafException(authTokenResponse.getContentAsString());
419 } catch (JsonSyntaxException e) {
420 logger.warn("Received invalid data", e);
421 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
422 "@text/error.nanoleaf.controller.invalidData");
423 } catch (NanoleafException e) {
424 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
425 "@text/error.nanoleaf.controller.noTokenReceived");
426 } catch (InterruptedException | ExecutionException | TimeoutException e) {
427 logger.warn("Cannot send authorization request to controller: ", e);
428 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
429 "@text/error.nanoleaf.controller.authRequest");
430 } catch (RuntimeException e) {
431 logger.warn("Pairing job failed", e);
432 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
433 } catch (Exception e) {
434 logger.warn("Cannot start http client", e);
435 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
436 "@text/error.nanoleaf.controller.noClient");
440 private void runPanelDiscovery() {
441 logger.debug("Run panel discovery job");
442 // Trigger a new discovery of connected panels
443 for (NanoleafControllerListener controllerListener : controllerListeners) {
445 controllerListener.onControllerInfoFetched(getThing().getUID(), receiveControllerInfo());
446 } catch (NanoleafUnauthorizedException nue) {
447 logger.warn("Panel discovery unauthorized: {}", nue.getMessage());
448 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
449 "@text/error.nanoleaf.controller.invalidToken");
450 if (StringUtils.isEmpty(getAuthToken())) {
451 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
452 "@text/error.nanoleaf.controller.noToken");
454 } catch (NanoleafInterruptedException nie) {
455 logger.info("Panel discovery has been stopped.");
456 } catch (NanoleafException ne) {
457 logger.warn("Failed to discover panels: ", ne);
458 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
459 "@text/error.nanoleaf.controller.communication");
460 } catch (RuntimeException e) {
461 logger.warn("Panel discovery job failed", e);
462 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/error.nanoleaf.controller.runtime");
468 * This is based on the touch event detection described in https://forum.nanoleaf.me/docs/openapi#_842h3097vbgq
470 private static boolean touchJobRunning = false;
472 private void runTouchDetection() {
473 if (touchJobRunning) {
474 logger.debug("touch job already running. quitting.");
478 touchJobRunning = true;
479 URI eventUri = OpenAPIUtils.getUri(getControllerConfig(), API_EVENTS, "id=4");
480 logger.debug("touch job registered on: {}", eventUri.toString());
481 httpClient.newRequest(eventUri).send(new Response.Listener.Adapter() // request runs forever
484 public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
485 String s = StandardCharsets.UTF_8.decode(content).toString();
486 logger.trace("content {}", s);
488 Scanner eventContent = new Scanner(s);
489 while (eventContent.hasNextLine()) {
490 String line = eventContent.nextLine().trim();
491 // we don't expect anything than content id:4, so we do not check that but only care about the
493 if (line.startsWith("data:")) {
494 String json = line.substring(5).trim(); // supposed to be JSON
497 TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class);
498 handleTouchEvents(touchEvents);
499 } catch (JsonSyntaxException jse) {
500 logger.error("couldn't parse touch event json {}", json);
504 eventContent.close();
505 logger.debug("leaving touch onContent");
506 super.onContent(response, content);
510 public void onSuccess(@Nullable Response response) {
511 logger.trace("touch event SUCCESS: {}", response);
515 public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
516 logger.trace("touch event FAILURE: {}", response);
520 public void onComplete(@Nullable Result result) {
521 logger.trace("touch event COMPLETE: {}", result);
524 } catch (RuntimeException | NanoleafException e) {
525 logger.warn("setting up TouchDetection failed", e);
527 touchJobRunning = false;
529 logger.debug("leaving run touch detection");
533 * Interate over all gathered touch events and apply them to the panel they belong to
537 private void handleTouchEvents(TouchEvents touchEvents) {
538 touchEvents.getEvents().forEach(event -> {
539 logger.info("panel: {} gesture id: {}", event.getPanelId(), event.getGesture());
541 // Iterate over all child things = all panels of that controller
542 this.getThing().getThings().forEach(child -> {
543 NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
544 if (panelHandler != null) {
545 logger.trace("Checking available panel -{}- versus event panel -{}-", panelHandler.getPanelID(),
547 if (panelHandler.getPanelID().equals(event.getPanelId())) {
548 logger.debug("Panel {} found. Triggering item with gesture {}.", panelHandler.getPanelID(),
550 panelHandler.updatePanelGesture(event.getGesture());
557 private void updateFromControllerInfo() throws NanoleafException {
558 logger.debug("Update channels for controller {}", thing.getUID());
559 this.controllerInfo = receiveControllerInfo();
560 if (controllerInfo == null) {
561 logger.debug("No Controller Info has been provided");
564 final State state = controllerInfo.getState();
566 OnOffType powerState = state.getOnOff();
567 updateState(CHANNEL_POWER, powerState);
570 Ct colorTemperature = state.getColorTemperature();
572 float colorTempPercent = 0f;
573 if (colorTemperature != null) {
574 updateState(CHANNEL_COLOR_TEMPERATURE_ABS, new DecimalType(colorTemperature.getValue()));
577 Integer min = colorTemperature.getMin();
578 int colorMin = (min == null) ? 0 : min;
581 Integer max = colorTemperature.getMax();
582 int colorMax = (max == null) ? 0 : max;
584 colorTempPercent = (colorTemperature.getValue() - colorMin) / (colorMax - colorMin)
585 * PercentType.HUNDRED.intValue();
588 updateState(CHANNEL_COLOR_TEMPERATURE, new PercentType(Float.toString(colorTempPercent)));
589 updateState(CHANNEL_EFFECT, new StringType(controllerInfo.getEffects().getSelect()));
592 Hue stateHue = state.getHue();
593 int hue = (stateHue != null) ? stateHue.getValue() : 0;
595 Sat stateSaturation = state.getSaturation();
596 int saturation = (stateSaturation != null) ? stateSaturation.getValue() : 0;
598 Brightness stateBrightness = state.getBrightness();
599 int brightness = (stateBrightness != null) ? stateBrightness.getValue() : 0;
601 updateState(CHANNEL_COLOR, new HSBType(new DecimalType(hue), new PercentType(saturation),
602 new PercentType(powerState == OnOffType.ON ? brightness : 0)));
603 updateState(CHANNEL_COLOR_MODE, new StringType(state.getColorMode()));
604 updateState(CHANNEL_RHYTHM_ACTIVE, controllerInfo.getRhythm().getRhythmActive() ? OnOffType.ON : OnOffType.OFF);
605 updateState(CHANNEL_RHYTHM_MODE, new DecimalType(controllerInfo.getRhythm().getRhythmMode()));
606 updateState(CHANNEL_RHYTHM_STATE,
607 controllerInfo.getRhythm().getRhythmConnected() ? OnOffType.ON : OnOffType.OFF);
608 // update bridge properties which may have changed, or are not present during discovery
609 Map<String, String> properties = editProperties();
610 properties.put(Thing.PROPERTY_SERIAL_NUMBER, controllerInfo.getSerialNo());
611 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, controllerInfo.getFirmwareVersion());
612 properties.put(Thing.PROPERTY_MODEL_ID, controllerInfo.getModel());
613 properties.put(Thing.PROPERTY_VENDOR, controllerInfo.getManufacturer());
614 updateProperties(properties);
616 Configuration config = editConfiguration();
618 if (MODEL_ID_CANVAS.equals(controllerInfo.getModel())) {
619 config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_CANVAS);
620 logger.debug("Set to device type {}", DEVICE_TYPE_CANVAS);
622 config.put(NanoleafControllerConfig.DEVICE_TYPE, DEVICE_TYPE_LIGHTPANELS);
623 logger.debug("Set to device type {}", DEVICE_TYPE_LIGHTPANELS);
625 updateConfiguration(config);
627 getConfig().getProperties().forEach((key, value) -> {
628 logger.trace("Configuration property: key {} value {}", key, value);
631 getThing().getProperties().forEach((key, value) -> {
632 logger.debug("Thing property: key {} value {}", key, value);
635 // update the color channels of each panel
636 this.getThing().getThings().forEach(child -> {
637 NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) child.getHandler();
638 if (panelHandler != null) {
639 logger.debug("Update color channel for panel {}", panelHandler.getThing().getUID());
640 panelHandler.updatePanelColorChannel();
645 private ControllerInfo receiveControllerInfo() throws NanoleafException, NanoleafUnauthorizedException {
646 ContentResponse controllerlInfoJSON = OpenAPIUtils.sendOpenAPIRequest(OpenAPIUtils.requestBuilder(httpClient,
647 getControllerConfig(), API_GET_CONTROLLER_INFO, HttpMethod.GET));
649 ControllerInfo controllerInfo = gson.fromJson(controllerlInfoJSON.getContentAsString(), ControllerInfo.class);
650 return controllerInfo;
653 private void sendStateCommand(String channel, Command command) throws NanoleafException {
654 State stateObject = new State();
657 if (command instanceof OnOffType) {
658 // On/Off command - turns controller on/off
659 BooleanState state = new On();
660 state.setValue(OnOffType.ON.equals(command));
661 stateObject.setState(state);
663 logger.warn("Unhandled command type: {}", command.getClass().getName());
668 if (command instanceof OnOffType) {
669 // On/Off command - turns controller on/off
670 BooleanState state = new On();
671 state.setValue(OnOffType.ON.equals(command));
672 stateObject.setState(state);
673 } else if (command instanceof HSBType) {
674 // regular color HSB command
675 IntegerState h = new Hue();
676 IntegerState s = new Sat();
677 IntegerState b = new Brightness();
678 h.setValue(((HSBType) command).getHue().intValue());
679 s.setValue(((HSBType) command).getSaturation().intValue());
680 b.setValue(((HSBType) command).getBrightness().intValue());
681 stateObject.setState(h);
682 stateObject.setState(s);
683 stateObject.setState(b);
684 } else if (command instanceof PercentType) {
685 // brightness command
686 IntegerState b = new Brightness();
687 b.setValue(((PercentType) command).intValue());
688 stateObject.setState(b);
689 } else if (command instanceof IncreaseDecreaseType) {
690 // increase/decrease brightness
691 if (controllerInfo != null) {
693 Brightness brightness = controllerInfo.getState().getBrightness();
694 int brightnessMin = 0;
695 int brightnessMax = 0;
696 if (brightness != null) {
698 Integer min = brightness.getMin();
699 brightnessMin = (min == null) ? 0 : min;
701 Integer max = brightness.getMax();
702 brightnessMax = (max == null) ? 0 : max;
704 if (IncreaseDecreaseType.INCREASE.equals(command)) {
706 Math.min(brightnessMax, brightness.getValue() + BRIGHTNESS_STEP_SIZE));
709 Math.max(brightnessMin, brightness.getValue() - BRIGHTNESS_STEP_SIZE));
711 stateObject.setState(brightness);
712 logger.debug("Setting controller brightness to {}", brightness.getValue());
713 // update controller info in case new command is sent before next update job interval
714 controllerInfo.getState().setBrightness(brightness);
716 logger.debug("Couldn't set brightness as it was null!");
720 logger.warn("Unhandled command type: {}", command.getClass().getName());
724 case CHANNEL_COLOR_TEMPERATURE:
725 if (command instanceof PercentType) {
726 // Color temperature (percent)
727 IntegerState state = new Ct();
729 Ct colorTemperature = controllerInfo.getState().getColorTemperature();
733 if (colorTemperature != null) {
735 Integer min = colorTemperature.getMin();
736 colorMin = (min == null) ? 0 : min;
739 Integer max = colorTemperature.getMax();
740 colorMax = (max == null) ? 0 : max;
743 state.setValue(Math.round((colorMax - colorMin) * ((PercentType) command).intValue()
744 / PercentType.HUNDRED.floatValue() + colorMin));
745 stateObject.setState(state);
747 logger.warn("Unhandled command type: {}", command.getClass().getName());
751 case CHANNEL_COLOR_TEMPERATURE_ABS:
752 if (command instanceof DecimalType) {
753 // Color temperature (absolute)
754 IntegerState state = new Ct();
755 state.setValue(((DecimalType) command).intValue());
756 stateObject.setState(state);
758 logger.warn("Unhandled command type: {}", command.getClass().getName());
762 case CHANNEL_PANEL_LAYOUT:
764 Layout layout = controllerInfo.getPanelLayout().getLayout();
765 String layoutView = (layout != null) ? layout.getLayoutView() : "";
766 logger.info("Panel layout and ids for controller {} \n{}", thing.getUID(), layoutView);
767 updateState(CHANNEL_PANEL_LAYOUT, OnOffType.OFF);
770 logger.warn("Unhandled command type: {}", command.getClass().getName());
774 Request setNewStateRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_SET_VALUE,
776 setNewStateRequest.content(new StringContentProvider(gson.toJson(stateObject)), "application/json");
777 OpenAPIUtils.sendOpenAPIRequest(setNewStateRequest);
780 private void sendEffectCommand(Command command) throws NanoleafException {
781 Effects effects = new Effects();
782 if (command instanceof StringType) {
783 effects.setSelect(command.toString());
785 logger.warn("Unhandled command type: {}", command.getClass().getName());
788 Request setNewEffectRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_EFFECT,
790 String content = gson.toJson(effects);
791 logger.debug("sending effect command from controller {}: {}", getThing().getUID(), content);
792 setNewEffectRequest.content(new StringContentProvider(content), "application/json");
793 OpenAPIUtils.sendOpenAPIRequest(setNewEffectRequest);
796 private void sendRhythmCommand(Command command) throws NanoleafException {
797 Rhythm rhythm = new Rhythm();
798 if (command instanceof DecimalType) {
799 rhythm.setRhythmMode(((DecimalType) command).intValue());
801 logger.warn("Unhandled command type: {}", command.getClass().getName());
804 Request setNewRhythmRequest = OpenAPIUtils.requestBuilder(httpClient, getControllerConfig(), API_RHYTHM_MODE,
806 setNewRhythmRequest.content(new StringContentProvider(gson.toJson(rhythm)), "application/json");
807 OpenAPIUtils.sendOpenAPIRequest(setNewRhythmRequest);
810 private String getAddress() {
811 return StringUtils.defaultString(this.address);
814 private void setAddress(String address) {
815 this.address = address;
818 private int getPort() {
822 private void setPort(int port) {
826 private int getRefreshIntervall() {
827 return refreshIntervall;
830 private void setRefreshIntervall(int refreshIntervall) {
831 this.refreshIntervall = refreshIntervall;
834 private String getAuthToken() {
835 return StringUtils.defaultString(authToken);
838 private void setAuthToken(@Nullable String authToken) {
839 this.authToken = authToken;
842 private String getDeviceType() {
843 return StringUtils.defaultString(deviceType);
846 private void setDeviceType(String deviceType) {
847 this.deviceType = deviceType;
850 private void stopAllJobs() {
853 stopPanelDiscoveryJob();