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