]> git.basschouten.com Git - openhab-addons.git/blob
bcd20df09ca169f7e2a6e09ba8bc16d3e9cfa48d
[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
14 package org.openhab.binding.mqtt.espmilighthub.internal.handler;
15
16 import static org.openhab.binding.mqtt.MqttBindingConstants.BINDING_ID;
17 import static org.openhab.binding.mqtt.espmilighthub.internal.EspMilightHubBindingConstants.*;
18
19 import java.math.BigDecimal;
20 import java.math.MathContext;
21 import java.nio.charset.StandardCharsets;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.mqtt.espmilighthub.internal.ConfigOptions;
26 import org.openhab.binding.mqtt.espmilighthub.internal.Helper;
27 import org.openhab.binding.mqtt.handler.AbstractBrokerHandler;
28 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
29 import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
30 import org.openhab.core.io.transport.mqtt.MqttConnectionState;
31 import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.HSBType;
34 import org.openhab.core.library.types.IncreaseDecreaseType;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.PercentType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingRegistry;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.ThingUID;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.State;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * The {@link EspMilightHubHandler} is responsible for handling commands of the globes, which are then
55  * sent to one of the bridges to be sent out by MQTT.
56  *
57  * @author Matthew Skinner - Initial contribution
58  */
59 @NonNullByDefault
60 public class EspMilightHubHandler extends BaseThingHandler implements MqttConnectionObserver, MqttMessageSubscriber {
61     // these are all constants used in color conversion calcuations.
62     // strings are necessary to prevent floating point loss of precision
63     private static final BigDecimal BIG_DECIMAL_THOUSAND = new BigDecimal(1000);
64     private static final BigDecimal BIG_DECIMAL_MILLION = new BigDecimal(1000000);
65
66     private static final BigDecimal[][] KANG_X_COEFFICIENTS = {
67             { new BigDecimal("-3.0258469"), new BigDecimal("2.1070379"), new BigDecimal("0.2226347"),
68                     new BigDecimal("0.24039") },
69             { new BigDecimal("-0.2661239"), new BigDecimal("-0.234589"), new BigDecimal("0.8776956"),
70                     new BigDecimal("0.179910") } };
71
72     private static final BigDecimal[][] KANG_Y_COEFFICIENTS = {
73             { new BigDecimal("3.0817580"), new BigDecimal("-5.8733867"), new BigDecimal("3.75112997"),
74                     new BigDecimal("-0.37001483") },
75             { new BigDecimal("-0.9549476"), new BigDecimal("-1.37418593"), new BigDecimal("2.09137015"),
76                     new BigDecimal("-0.16748867") },
77             { new BigDecimal("-1.1063814"), new BigDecimal("-1.34811020"), new BigDecimal("2.18555832"),
78                     new BigDecimal("-0.20219683") } };
79
80     private static final BigDecimal BIG_DECIMAL_03320 = new BigDecimal("0.3320");
81     private static final BigDecimal BIG_DECIMAL_01858 = new BigDecimal("0.1858");
82     private static final BigDecimal[] MCCAMY_COEFFICIENTS = { new BigDecimal(437), new BigDecimal(3601),
83             new BigDecimal(6862), new BigDecimal(5517) };
84
85     private static final BigDecimal BIG_DECIMAL_2 = new BigDecimal(2);
86     private static final BigDecimal BIG_DECIMAL_3 = new BigDecimal(3);
87     private static final BigDecimal BIG_DECIMAL_4 = new BigDecimal(4);
88     private static final BigDecimal BIG_DECIMAL_6 = new BigDecimal(6);
89     private static final BigDecimal BIG_DECIMAL_12 = new BigDecimal(12);
90
91     private static final BigDecimal BIG_DECIMAL_0292 = new BigDecimal("0.292");
92     private static final BigDecimal BIG_DECIMAL_024 = new BigDecimal("0.24");
93
94     private static final BigDecimal[] CORM_COEFFICIENTS = { new BigDecimal("-0.00616793"), new BigDecimal("0.0893944"),
95             new BigDecimal("-0.5179722"), new BigDecimal("1.5317403"), new BigDecimal("-2.4243787"),
96             new BigDecimal("1.925865"), new BigDecimal("-0.471106") };
97
98     private static final BigDecimal BIG_DECIMAL_153 = new BigDecimal(153);
99     private static final BigDecimal BIG_DECIMAL_217 = new BigDecimal(217);
100
101     private final Logger logger = LoggerFactory.getLogger(this.getClass());
102     private @Nullable MqttBrokerConnection connection;
103     private ThingRegistry thingRegistry;
104     private String globeType = "";
105     private String bulbMode = "";
106     private String remotesGroupID = "";
107     private String channelPrefix = "";
108     private String fullCommandTopic = "";
109     private String fullStatesTopic = "";
110     private BigDecimal maxColourTemp = BigDecimal.ZERO;
111     private BigDecimal minColourTemp = BigDecimal.ZERO;
112     private BigDecimal savedLevel = BIG_DECIMAL_100;
113     private ConfigOptions config = new ConfigOptions();
114
115     public EspMilightHubHandler(Thing thing, ThingRegistry thingRegistry) {
116         super(thing);
117         this.thingRegistry = thingRegistry;
118     }
119
120     void changeChannel(String channel, State state) {
121         updateState(new ChannelUID(channelPrefix + channel), state);
122         // Remote code of 0 means that all channels need to follow this change.
123         if ("0".equals(remotesGroupID)) {
124             switch (globeType) {
125                 // These two are 8 channel remotes
126                 case "fut091":
127                 case "fut089":
128                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "5:" + channel),
129                             state);
130                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "6:" + channel),
131                             state);
132                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "7:" + channel),
133                             state);
134                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "8:" + channel),
135                             state);
136                 default:
137                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "1:" + channel),
138                             state);
139                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "2:" + channel),
140                             state);
141                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "3:" + channel),
142                             state);
143                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "4:" + channel),
144                             state);
145             }
146         }
147     }
148
149     private void processIncomingState(String messageJSON) {
150         // Need to handle State and Level at the same time to process level=0 as off//
151         PercentType tempBulbLevel = PercentType.ZERO;
152         String bulbState = Helper.resolveJSON(messageJSON, "\"state\":\"", 3);
153         String bulbLevel = Helper.resolveJSON(messageJSON, "\"level\":", 3);
154         if (!bulbLevel.isEmpty()) {
155             if ("0".equals(bulbLevel) || "OFF".equals(bulbState)) {
156                 changeChannel(CHANNEL_LEVEL, OnOffType.OFF);
157             } else {
158                 tempBulbLevel = new PercentType(Integer.valueOf(bulbLevel));
159                 changeChannel(CHANNEL_LEVEL, tempBulbLevel);
160             }
161         } else if ("ON".equals(bulbState) || "OFF".equals(bulbState)) { // NOTE: Level is missing when this runs
162             changeChannel(CHANNEL_LEVEL, OnOffType.valueOf(bulbState));
163         }
164         bulbMode = Helper.resolveJSON(messageJSON, "\"bulb_mode\":\"", 5);
165         switch (bulbMode) {
166             case "white":
167                 if (hasRGB()) {
168                     changeChannel(CHANNEL_BULB_MODE, new StringType("white"));
169                     changeChannel(CHANNEL_DISCO_MODE, new StringType("None"));
170                 }
171                 String bulbCTempS = Helper.resolveJSON(messageJSON, "\"color_temp\":", 3);
172                 if (!bulbCTempS.isEmpty()) {
173                     var bulbCTemp = Integer.valueOf(bulbCTempS);
174                     changeChannel(CHANNEL_COLOURTEMP, scaleMireds(bulbCTemp));
175                     if (hasRGB()) {
176                         changeChannel(CHANNEL_COLOUR, calculateHSBFromColorTemp(bulbCTemp, tempBulbLevel));
177                     }
178                 }
179                 break;
180             case "color":
181                 changeChannel(CHANNEL_BULB_MODE, new StringType("color"));
182                 changeChannel(CHANNEL_DISCO_MODE, new StringType("None"));
183                 String bulbHue = Helper.resolveJSON(messageJSON, "\"hue\":", 3);
184                 String bulbSaturation = Helper.resolveJSON(messageJSON, "\"saturation\":", 3);
185                 if (bulbHue.isEmpty()) {
186                     logger.warn("Milight MQTT message came in as being a colour mode, but was missing a HUE value.");
187                 } else {
188                     if (bulbSaturation.isEmpty()) {
189                         bulbSaturation = "100";
190                     }
191                     // 360 isn't allowed by OpenHAB
192                     if (bulbHue.equals("360")) {
193                         bulbHue = "0";
194                     }
195                     var hsb = new HSBType(new DecimalType(Integer.valueOf(bulbHue)),
196                             new PercentType(Integer.valueOf(bulbSaturation)), tempBulbLevel);
197                     changeChannel(CHANNEL_COLOUR, hsb);
198                     if (hasCCT()) {
199                         changeChannel(CHANNEL_COLOURTEMP, scaleMireds(calculateColorTempFromHSB(hsb)));
200                     }
201                 }
202                 break;
203             case "scene":
204                 if (hasRGB()) {
205                     changeChannel(CHANNEL_BULB_MODE, new StringType("scene"));
206                 }
207                 String bulbDiscoMode = Helper.resolveJSON(messageJSON, "\"mode\":", 1);
208                 if (!bulbDiscoMode.isEmpty()) {
209                     changeChannel(CHANNEL_DISCO_MODE, new StringType(bulbDiscoMode));
210                 }
211                 break;
212             case "night":
213                 if (hasRGB()) {
214                     changeChannel(CHANNEL_BULB_MODE, new StringType("night"));
215                     if (config.oneTriggersNightMode) {
216                         changeChannel(CHANNEL_LEVEL, new PercentType("1"));
217                     }
218                 }
219                 break;
220         }
221     }
222
223     private boolean hasCCT() {
224         switch (globeType) {
225             case "rgb_cct":
226             case "cct":
227             case "fut089":
228             case "fut091":
229                 return true;
230             default:
231                 return false;
232         }
233     }
234
235     private boolean hasRGB() {
236         switch (globeType) {
237             case "rgb_cct":
238             case "rgb":
239             case "rgbw":
240             case "fut089":
241                 return true;
242             default:
243                 return false;
244         }
245     }
246
247     /**
248      * Scales mireds to 0-100%
249      */
250     private static PercentType scaleMireds(int mireds) {
251         // range in mireds is 153-370
252         // 100 - (mireds - 153) / (370 - 153) * 100
253         if (mireds >= 370) {
254             return PercentType.HUNDRED;
255         } else if (mireds <= 153) {
256             return PercentType.ZERO;
257         }
258         return new PercentType(BIG_DECIMAL_100.subtract(new BigDecimal(mireds).subtract(BIG_DECIMAL_153)
259                 .divide(BIG_DECIMAL_217, MathContext.DECIMAL128).multiply(BIG_DECIMAL_100)));
260     }
261
262     private static BigDecimal polynomialFit(BigDecimal x, BigDecimal[] coefficients) {
263         var result = BigDecimal.ZERO;
264         var xAccumulator = BigDecimal.ONE;
265         // forms K[4]*x^0 + K[3]*x^1 + K[2]*x^2 + K[1]*x^3 + K[0]*x^4
266         // (or reverse the order of terms for the usual way of writing it in academic papers)
267         for (int i = coefficients.length - 1; i >= 0; i--) {
268             result = result.add(coefficients[i].multiply(xAccumulator));
269             xAccumulator = xAccumulator.multiply(x);
270         }
271         return result;
272     }
273
274     // https://www.jkps.or.kr/journal/download_pdf.php?spage=865&volume=41&number=6 (8) and (9)
275     private static HSBType calculateHSBFromColorTemp(int mireds, PercentType brightness) {
276         var cct = BIG_DECIMAL_MILLION.divide(new BigDecimal(mireds), MathContext.DECIMAL128);
277         var cctInt = cct.intValue();
278
279         BigDecimal[] coefficients;
280         // 1667K to 4000K and 4000K to 25000K; no range checks since our mired range fits within this
281         if (cctInt <= 4000) {
282             coefficients = KANG_X_COEFFICIENTS[1];
283         } else {
284             coefficients = KANG_X_COEFFICIENTS[0];
285         }
286         BigDecimal x = polynomialFit(BIG_DECIMAL_THOUSAND.divide(cct, MathContext.DECIMAL128), coefficients);
287
288         if (cctInt <= 2222) {
289             coefficients = KANG_Y_COEFFICIENTS[2];
290         } else if (cctInt <= 4000) {
291             coefficients = KANG_Y_COEFFICIENTS[1];
292         } else {
293             coefficients = KANG_Y_COEFFICIENTS[0];
294         }
295         BigDecimal y = polynomialFit(x, coefficients);
296         var rawHsb = HSBType.fromXY(x.floatValue() * 100.0f, y.floatValue() * 100.0f);
297         return new HSBType(rawHsb.getHue(), rawHsb.getSaturation(), brightness);
298     }
299
300     // https://www.waveformlighting.com/tech/calculate-color-temperature-cct-from-cie-1931-xy-coordinates/
301     private static int calculateColorTempFromHSB(HSBType hsb) {
302         PercentType[] xy = hsb.toXY();
303         var x = xy[0].toBigDecimal().divide(BIG_DECIMAL_100);
304         var y = xy[1].toBigDecimal().divide(BIG_DECIMAL_100);
305         var n = x.subtract(BIG_DECIMAL_03320).divide(BIG_DECIMAL_01858.subtract(y), MathContext.DECIMAL128);
306         BigDecimal cctK = polynomialFit(n, MCCAMY_COEFFICIENTS);
307         return BIG_DECIMAL_MILLION.divide(cctK, MathContext.DECIMAL128).round(new MathContext(0)).intValue();
308     }
309
310     // https://cormusa.org/wp-content/uploads/2018/04/CORM_2011_Calculation_of_CCT_and_Duv_and_Practical_Conversion_Formulae.pdf
311     // page 19
312     private static BigDecimal calculateDuvFromHSB(HSBType hsb) {
313         PercentType[] xy = hsb.toXY();
314         var x = xy[0].toBigDecimal().divide(BIG_DECIMAL_100);
315         var y = xy[1].toBigDecimal().divide(BIG_DECIMAL_100);
316         var u = BIG_DECIMAL_4.multiply(x).divide(
317                 BIG_DECIMAL_2.multiply(x).negate().add(BIG_DECIMAL_12.multiply(y).add(BIG_DECIMAL_3)),
318                 MathContext.DECIMAL128);
319         var v = BIG_DECIMAL_6.multiply(y).divide(
320                 BIG_DECIMAL_2.multiply(x).negate().add(BIG_DECIMAL_12.multiply(y).add(BIG_DECIMAL_3)),
321                 MathContext.DECIMAL128);
322         var Lfp = u.subtract(BIG_DECIMAL_0292).pow(2).add(v.subtract(BIG_DECIMAL_024).pow(2))
323                 .sqrt(MathContext.DECIMAL128);
324         var a = new BigDecimal(
325                 Math.acos(u.subtract(BIG_DECIMAL_0292).divide(Lfp, MathContext.DECIMAL128).doubleValue()));
326         BigDecimal Lbb = polynomialFit(a, CORM_COEFFICIENTS);
327         return Lfp.subtract(Lbb);
328     }
329
330     /*
331      * Used to calculate the colour temp for a globe if you want the light to get warmer as it is dimmed like a
332      * traditional halogen globe
333      */
334     private int autoColourTemp(int brightness) {
335         return minColourTemp.subtract(
336                 minColourTemp.subtract(maxColourTemp).divide(BIG_DECIMAL_100).multiply(new BigDecimal(brightness)))
337                 .intValue();
338     }
339
340     void turnOff() {
341         if (config.powerFailsToMinimum) {
342             sendMQTT("{\"state\":\"OFF\",\"level\":0}");
343         } else {
344             sendMQTT("{\"state\":\"OFF\"}");
345         }
346     }
347
348     void handleLevelColour(Command command) {
349         int mireds;
350
351         if (command instanceof OnOffType) {
352             if (OnOffType.ON.equals(command)) {
353                 sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel + "}");
354                 return;
355             } else {
356                 turnOff();
357             }
358         } else if (command instanceof IncreaseDecreaseType) {
359             if (IncreaseDecreaseType.INCREASE.equals(command)) {
360                 if (savedLevel.intValue() <= 90) {
361                     savedLevel = savedLevel.add(BigDecimal.TEN);
362                 }
363             } else {
364                 if (savedLevel.intValue() >= 10) {
365                     savedLevel = savedLevel.subtract(BigDecimal.TEN);
366                 }
367             }
368             sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel.intValue() + "}");
369             return;
370         } else if (command instanceof HSBType) {
371             HSBType hsb = (HSBType) command;
372             // This feature allows google home or Echo to trigger white mode when asked to turn color to white.
373             if (hsb.getHue().intValue() == config.whiteHue && hsb.getSaturation().intValue() == config.whiteSat) {
374                 if (hasCCT()) {
375                     sendMQTT("{\"state\":\"ON\",\"color_temp\":" + config.favouriteWhite + "}");
376                 } else {// globe must only have 1 type of white
377                     sendMQTT("{\"command\":\"set_white\"}");
378                 }
379                 return;
380             } else if (PercentType.ZERO.equals(hsb.getBrightness())) {
381                 turnOff();
382                 return;
383             } else if (config.duvThreshold.compareTo(BigDecimal.ONE) < 0
384                     && calculateDuvFromHSB(hsb).abs().compareTo(config.duvThreshold) <= 0
385                     && (mireds = calculateColorTempFromHSB(hsb)) >= 153 && mireds <= 370) {
386                 sendMQTT("{\"state\":\"ON\",\"level\":" + hsb.getBrightness() + ",\"color_temp\":" + mireds + "}");
387             } else if (config.whiteThreshold != -1 && hsb.getSaturation().intValue() <= config.whiteThreshold) {
388                 sendMQTT("{\"command\":\"set_white\"}");// Can't send the command and level in the same message.
389                 sendMQTT("{\"level\":" + hsb.getBrightness().intValue() + "}");
390             } else {
391                 sendMQTT("{\"state\":\"ON\",\"level\":" + hsb.getBrightness().intValue() + ",\"hue\":"
392                         + hsb.getHue().intValue() + ",\"saturation\":" + hsb.getSaturation().intValue() + "}");
393             }
394             savedLevel = hsb.getBrightness().toBigDecimal();
395             return;
396         } else if (command instanceof PercentType) {
397             PercentType percentType = (PercentType) command;
398             if (percentType.intValue() == 0) {
399                 turnOff();
400                 return;
401             } else if (percentType.intValue() == 1 && config.oneTriggersNightMode) {
402                 sendMQTT("{\"command\":\"night_mode\"}");
403                 return;
404             }
405             sendMQTT("{\"state\":\"ON\",\"level\":" + command + "}");
406             savedLevel = percentType.toBigDecimal();
407             if (hasCCT()) {
408                 if (config.dimmedCT > 0 && "white".equals(bulbMode)) {
409                     sendMQTT("{\"state\":\"ON\",\"color_temp\":" + autoColourTemp(savedLevel.intValue()) + "}");
410                 }
411             }
412             return;
413         }
414     }
415
416     @Override
417     public void handleCommand(ChannelUID channelUID, Command command) {
418         if (command instanceof RefreshType) {
419             return;
420         }
421         switch (channelUID.getId()) {
422             case CHANNEL_COLOUR:
423             case CHANNEL_LEVEL:
424                 handleLevelColour(command);
425                 break;
426             case CHANNEL_BULB_MODE:
427                 bulbMode = command.toString();
428                 break;
429             case CHANNEL_COLOURTEMP:
430                 int scaledCommand = (int) Math.round((370 - (2.17 * Float.valueOf(command.toString()))));
431                 sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel + ",\"color_temp\":" + scaledCommand + "}");
432                 break;
433             case CHANNEL_COMMAND:
434                 sendMQTT("{\"command\":\"" + command + "\"}");
435                 break;
436             case CHANNEL_DISCO_MODE:
437                 sendMQTT("{\"mode\":\"" + command + "\"}");
438                 break;
439         }
440     }
441
442     @Override
443     public void initialize() {
444         config = getConfigAs(ConfigOptions.class);
445         if (config.dimmedCT > 0) {
446             maxColourTemp = new BigDecimal(config.favouriteWhite);
447             minColourTemp = new BigDecimal(config.dimmedCT);
448             if (minColourTemp.intValue() <= maxColourTemp.intValue()) {
449                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
450                         "The dimmedCT config value must be greater than the favourite White value.");
451                 return;
452             }
453         }
454         Bridge localBridge = getBridge();
455         if (localBridge == null) {
456             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
457                     "Globe must have a valid bridge selected before it can come online.");
458             return;
459         } else {
460             globeType = thing.getThingTypeUID().getId();// eg rgb_cct
461             String globeLocation = this.getThing().getUID().getId();// eg 0x014
462             remotesGroupID = globeLocation.substring(globeLocation.length() - 1, globeLocation.length());// eg 4
463             String remotesIDCode = globeLocation.substring(0, globeLocation.length() - 1);// eg 0x01
464             fullCommandTopic = COMMANDS_BASE_TOPIC + remotesIDCode + "/" + globeType + "/" + remotesGroupID;
465             fullStatesTopic = STATES_BASE_TOPIC + remotesIDCode + "/" + globeType + "/" + remotesGroupID;
466             // Need to remove the lowercase x from 0x12AB in case it contains all numbers
467             String caseCheck = globeLocation.substring(2, globeLocation.length() - 1);
468             if (!caseCheck.equals(caseCheck.toUpperCase())) {
469                 logger.warn(
470                         "The milight globe {}{} is using lowercase for the remote code when the hub needs UPPERCASE",
471                         remotesIDCode, remotesGroupID);
472             }
473             channelPrefix = BINDING_ID + ":" + globeType + ":" + localBridge.getUID().getId() + ":" + remotesIDCode
474                     + remotesGroupID + ":";
475             connectMQTT();
476         }
477     }
478
479     private void sendMQTT(String payload) {
480         MqttBrokerConnection localConnection = connection;
481         if (localConnection != null) {
482             localConnection.publish(fullCommandTopic, payload.getBytes(), 1, false);
483         }
484     }
485
486     @Override
487     public void processMessage(String topic, byte[] payload) {
488         String state = new String(payload, StandardCharsets.UTF_8);
489         logger.trace("Received the following new Milight state:{}:{}", topic, state);
490         try {
491             processIncomingState(state);
492         } catch (Exception e) {
493             logger.warn("Failed processing Milight state {} for {}", state, topic, e);
494         }
495     }
496
497     @Override
498     public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
499         logger.debug("MQTT brokers state changed to:{}", state);
500         switch (state) {
501             case CONNECTED:
502                 updateStatus(ThingStatus.ONLINE);
503                 break;
504             case CONNECTING:
505             case DISCONNECTED:
506                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
507                         "Bridge (broker) is not connected to your MQTT broker.");
508         }
509     }
510
511     public void connectMQTT() {
512         Bridge localBridge = this.getBridge();
513         if (localBridge == null) {
514             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
515                     "Bridge is missing or offline, you need to setup a working MQTT broker first.");
516             return;
517         }
518         ThingUID thingUID = localBridge.getUID();
519         Thing thing = thingRegistry.get(thingUID);
520         if (thing == null) {
521             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
522                     "Bridge is missing or offline, you need to setup a working MQTT broker first.");
523             return;
524         }
525         ThingHandler handler = thing.getHandler();
526         if (handler instanceof AbstractBrokerHandler) {
527             AbstractBrokerHandler abh = (AbstractBrokerHandler) handler;
528             MqttBrokerConnection localConnection = abh.getConnection();
529             if (localConnection != null) {
530                 localConnection.setKeepAliveInterval(20);
531                 localConnection.setQos(1);
532                 localConnection.setUnsubscribeOnStop(true);
533                 localConnection.addConnectionObserver(this);
534                 localConnection.start();
535                 localConnection.subscribe(fullStatesTopic + "/#", this);
536                 connection = localConnection;
537                 if (localConnection.connectionState().compareTo(MqttConnectionState.CONNECTED) == 0) {
538                     updateStatus(ThingStatus.ONLINE);
539                 }
540             }
541         }
542         return;
543     }
544
545     @Override
546     public void dispose() {
547         MqttBrokerConnection localConnection = connection;
548         if (localConnection != null) {
549             localConnection.unsubscribe(fullStatesTopic + "/#", this);
550         }
551     }
552 }