]> git.basschouten.com Git - openhab-addons.git/blob
e45475164a6c598bb329132ec00704b557f3d4a5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.nio.charset.StandardCharsets;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.mqtt.espmilighthub.internal.ConfigOptions;
25 import org.openhab.binding.mqtt.espmilighthub.internal.Helper;
26 import org.openhab.binding.mqtt.handler.AbstractBrokerHandler;
27 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
28 import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
29 import org.openhab.core.io.transport.mqtt.MqttConnectionState;
30 import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
31 import org.openhab.core.library.types.HSBType;
32 import org.openhab.core.library.types.IncreaseDecreaseType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.PercentType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingRegistry;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingUID;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.thing.binding.ThingHandler;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.State;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * The {@link EspMilightHubHandler} is responsible for handling commands of the globes, which are then
53  * sent to one of the bridges to be sent out by MQTT.
54  *
55  * @author Matthew Skinner - Initial contribution
56  */
57 @NonNullByDefault
58 public class EspMilightHubHandler extends BaseThingHandler implements MqttConnectionObserver, MqttMessageSubscriber {
59     private final Logger logger = LoggerFactory.getLogger(this.getClass());
60     private @Nullable MqttBrokerConnection connection;
61     private ThingRegistry thingRegistry;
62     private String globeType = "";
63     private String bulbMode = "";
64     private String remotesGroupID = "";
65     private String channelPrefix = "";
66     private String fullCommandTopic = "";
67     private String fullStatesTopic = "";
68     private BigDecimal maxColourTemp = BigDecimal.ZERO;
69     private BigDecimal minColourTemp = BigDecimal.ZERO;
70     private BigDecimal savedLevel = BIG_DECIMAL_100;
71     private ConfigOptions config = new ConfigOptions();
72
73     public EspMilightHubHandler(Thing thing, ThingRegistry thingRegistry) {
74         super(thing);
75         this.thingRegistry = thingRegistry;
76     }
77
78     void changeChannel(String channel, State state) {
79         updateState(new ChannelUID(channelPrefix + channel), state);
80         // Remote code of 0 means that all channels need to follow this change.
81         if ("0".equals(remotesGroupID)) {
82             switch (globeType) {
83                 // These two are 8 channel remotes
84                 case "fut091":
85                 case "fut089":
86                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "5:" + channel),
87                             state);
88                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "6:" + channel),
89                             state);
90                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "7:" + channel),
91                             state);
92                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "8:" + channel),
93                             state);
94                 default:
95                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "1:" + channel),
96                             state);
97                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "2:" + channel),
98                             state);
99                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "3:" + channel),
100                             state);
101                     updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "4:" + channel),
102                             state);
103             }
104         }
105     }
106
107     private void processIncomingState(String messageJSON) {
108         // Need to handle State and Level at the same time to process level=0 as off//
109         BigDecimal tempBulbLevel = BigDecimal.ZERO;
110         String bulbState = Helper.resolveJSON(messageJSON, "\"state\":\"", 3);
111         String bulbLevel = Helper.resolveJSON(messageJSON, "\"level\":", 3);
112         if (!bulbLevel.isEmpty()) {
113             if ("0".equals(bulbLevel) || "OFF".equals(bulbState)) {
114                 changeChannel(CHANNEL_LEVEL, OnOffType.OFF);
115                 tempBulbLevel = BigDecimal.ZERO;
116             } else {
117                 tempBulbLevel = new BigDecimal(bulbLevel);
118                 changeChannel(CHANNEL_LEVEL, new PercentType(tempBulbLevel));
119             }
120         } else if ("ON".equals(bulbState) || "OFF".equals(bulbState)) { // NOTE: Level is missing when this runs
121             changeChannel(CHANNEL_LEVEL, OnOffType.valueOf(bulbState));
122         }
123         bulbMode = Helper.resolveJSON(messageJSON, "\"bulb_mode\":\"", 5);
124         switch (bulbMode) {
125             case "white":
126                 if (!"cct".equals(globeType) && !"fut091".equals(globeType)) {
127                     changeChannel(CHANNEL_BULB_MODE, new StringType("white"));
128                     changeChannel(CHANNEL_COLOUR, new HSBType("0,0," + tempBulbLevel));
129                     changeChannel(CHANNEL_DISCO_MODE, new StringType("None"));
130                 }
131                 String bulbCTemp = Helper.resolveJSON(messageJSON, "\"color_temp\":", 3);
132                 if (!bulbCTemp.isEmpty()) {
133                     int ibulbCTemp = (int) Math.round(((Float.valueOf(bulbCTemp) / 2.17) - 171) * -1);
134                     changeChannel(CHANNEL_COLOURTEMP, new PercentType(ibulbCTemp));
135                 }
136                 break;
137             case "color":
138                 changeChannel(CHANNEL_BULB_MODE, new StringType("color"));
139                 changeChannel(CHANNEL_DISCO_MODE, new StringType("None"));
140                 String bulbHue = Helper.resolveJSON(messageJSON, "\"hue\":", 3);
141                 String bulbSaturation = Helper.resolveJSON(messageJSON, "\"saturation\":", 3);
142                 if (bulbHue.isEmpty()) {
143                     logger.warn("Milight MQTT message came in as being a colour mode, but was missing a HUE value.");
144                 } else {
145                     if (bulbSaturation.isEmpty()) {
146                         bulbSaturation = "100";
147                     }
148                     changeChannel(CHANNEL_COLOUR, new HSBType(bulbHue + "," + bulbSaturation + "," + tempBulbLevel));
149                 }
150                 break;
151             case "scene":
152                 if (!"cct".equals(globeType) && !"fut091".equals(globeType)) {
153                     changeChannel(CHANNEL_BULB_MODE, new StringType("scene"));
154                 }
155                 String bulbDiscoMode = Helper.resolveJSON(messageJSON, "\"mode\":", 1);
156                 if (!bulbDiscoMode.isEmpty()) {
157                     changeChannel(CHANNEL_DISCO_MODE, new StringType(bulbDiscoMode));
158                 }
159                 break;
160             case "night":
161                 if (!"cct".equals(globeType) && !"fut091".equals(globeType)) {
162                     changeChannel(CHANNEL_BULB_MODE, new StringType("night"));
163                     if (config.oneTriggersNightMode) {
164                         changeChannel(CHANNEL_LEVEL, new PercentType("1"));
165                     }
166                 }
167                 break;
168         }
169     }
170
171     /*
172      * Used to calculate the colour temp for a globe if you want the light to get warmer as it is dimmed like a
173      * traditional halogen globe
174      */
175     private int autoColourTemp(int brightness) {
176         return minColourTemp.subtract(
177                 minColourTemp.subtract(maxColourTemp).divide(BIG_DECIMAL_100).multiply(new BigDecimal(brightness)))
178                 .intValue();
179     }
180
181     void turnOff() {
182         if (config.powerFailsToMinimum) {
183             sendMQTT("{\"state\":\"OFF\",\"level\":0}");
184         } else {
185             sendMQTT("{\"state\":\"OFF\"}");
186         }
187     }
188
189     void handleLevelColour(Command command) {
190         if (command instanceof OnOffType) {
191             if (OnOffType.ON.equals(command)) {
192                 sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel + "}");
193                 return;
194             } else {
195                 turnOff();
196             }
197         } else if (command instanceof IncreaseDecreaseType) {
198             if (IncreaseDecreaseType.INCREASE.equals(command)) {
199                 if (savedLevel.intValue() <= 90) {
200                     savedLevel = savedLevel.add(BigDecimal.TEN);
201                 }
202             } else {
203                 if (savedLevel.intValue() >= 10) {
204                     savedLevel = savedLevel.subtract(BigDecimal.TEN);
205                 }
206             }
207             sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel.intValue() + "}");
208             return;
209         } else if (command instanceof HSBType) {
210             HSBType hsb = (HSBType) command;
211             // This feature allows google home or Echo to trigger white mode when asked to turn color to white.
212             if (hsb.getHue().intValue() == config.whiteHue && hsb.getSaturation().intValue() == config.whiteSat) {
213                 if ("rgb_cct".equals(globeType) || "fut089".equals(globeType)) {
214                     sendMQTT("{\"state\":\"ON\",\"color_temp\":" + config.favouriteWhite + "}");
215                 } else {// globe must only have 1 type of white
216                     sendMQTT("{\"command\":\"set_white\"}");
217                 }
218                 return;
219             } else if (PercentType.ZERO.equals(hsb.getBrightness())) {
220                 turnOff();
221                 return;
222             } else if (config.whiteThreshold != -1 && hsb.getSaturation().intValue() <= config.whiteThreshold) {
223                 sendMQTT("{\"command\":\"set_white\"}");// Can't send the command and level in the same message.
224                 sendMQTT("{\"level\":" + hsb.getBrightness().intValue() + "}");
225             } else {
226                 sendMQTT("{\"state\":\"ON\",\"level\":" + hsb.getBrightness().intValue() + ",\"hue\":"
227                         + hsb.getHue().intValue() + ",\"saturation\":" + hsb.getSaturation().intValue() + "}");
228             }
229             savedLevel = hsb.getBrightness().toBigDecimal();
230             return;
231         } else if (command instanceof PercentType) {
232             PercentType percentType = (PercentType) command;
233             if (percentType.intValue() == 0) {
234                 turnOff();
235                 return;
236             } else if (percentType.intValue() == 1 && config.oneTriggersNightMode) {
237                 sendMQTT("{\"command\":\"night_mode\"}");
238                 return;
239             }
240             sendMQTT("{\"state\":\"ON\",\"level\":" + command + "}");
241             savedLevel = percentType.toBigDecimal();
242             if ("rgb_cct".equals(globeType) || "fut089".equals(globeType)) {
243                 if (config.dimmedCT > 0 && "white".equals(bulbMode)) {
244                     sendMQTT("{\"state\":\"ON\",\"color_temp\":" + autoColourTemp(savedLevel.intValue()) + "}");
245                 }
246             }
247             return;
248         }
249     }
250
251     @Override
252     public void handleCommand(ChannelUID channelUID, Command command) {
253         if (command instanceof RefreshType) {
254             return;
255         }
256         switch (channelUID.getId()) {
257             case CHANNEL_LEVEL:
258                 handleLevelColour(command);
259                 return;
260             case CHANNEL_BULB_MODE:
261                 bulbMode = command.toString();
262                 break;
263             case CHANNEL_COLOURTEMP:
264                 int scaledCommand = (int) Math.round((370 - (2.17 * Float.valueOf(command.toString()))));
265                 sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel + ",\"color_temp\":" + scaledCommand + "}");
266                 break;
267             case CHANNEL_COMMAND:
268                 sendMQTT("{\"command\":\"" + command + "\"}");
269                 break;
270             case CHANNEL_DISCO_MODE:
271                 sendMQTT("{\"mode\":\"" + command + "\"}");
272                 break;
273             case CHANNEL_COLOUR:
274                 handleLevelColour(command);
275         }
276     }
277
278     @Override
279     public void initialize() {
280         config = getConfigAs(ConfigOptions.class);
281         if (config.dimmedCT > 0) {
282             maxColourTemp = new BigDecimal(config.favouriteWhite);
283             minColourTemp = new BigDecimal(config.dimmedCT);
284             if (minColourTemp.intValue() <= maxColourTemp.intValue()) {
285                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
286                         "The dimmedCT config value must be greater than the favourite White value.");
287                 return;
288             }
289         }
290         Bridge localBridge = getBridge();
291         if (localBridge == null) {
292             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
293                     "Globe must have a valid bridge selected before it can come online.");
294             return;
295         } else {
296             globeType = thing.getThingTypeUID().getId();// eg rgb_cct
297             String globeLocation = this.getThing().getUID().getId();// eg 0x014
298             remotesGroupID = globeLocation.substring(globeLocation.length() - 1, globeLocation.length());// eg 4
299             String remotesIDCode = globeLocation.substring(0, globeLocation.length() - 1);// eg 0x01
300             fullCommandTopic = COMMANDS_BASE_TOPIC + remotesIDCode + "/" + globeType + "/" + remotesGroupID;
301             fullStatesTopic = STATES_BASE_TOPIC + remotesIDCode + "/" + globeType + "/" + remotesGroupID;
302             // Need to remove the lowercase x from 0x12AB in case it contains all numbers
303             String caseCheck = globeLocation.substring(2, globeLocation.length() - 1);
304             if (!caseCheck.equals(caseCheck.toUpperCase())) {
305                 logger.warn(
306                         "The milight globe {}{} is using lowercase for the remote code when the hub needs UPPERCASE",
307                         remotesIDCode, remotesGroupID);
308             }
309             channelPrefix = BINDING_ID + ":" + globeType + ":" + localBridge.getUID().getId() + ":" + remotesIDCode
310                     + remotesGroupID + ":";
311             connectMQTT();
312         }
313     }
314
315     private void sendMQTT(String payload) {
316         MqttBrokerConnection localConnection = connection;
317         if (localConnection != null) {
318             localConnection.publish(fullCommandTopic, payload.getBytes(), 1, false);
319         }
320     }
321
322     @Override
323     public void processMessage(String topic, byte[] payload) {
324         String state = new String(payload, StandardCharsets.UTF_8);
325         logger.trace("Recieved the following new Milight state:{}:{}", topic, state);
326         processIncomingState(state);
327     }
328
329     @Override
330     public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
331         logger.debug("MQTT brokers state changed to:{}", state);
332         switch (state) {
333             case CONNECTED:
334                 updateStatus(ThingStatus.ONLINE);
335                 break;
336             case CONNECTING:
337             case DISCONNECTED:
338                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
339                         "Bridge (broker) is not connected to your MQTT broker.");
340         }
341     }
342
343     public void connectMQTT() {
344         Bridge localBridge = this.getBridge();
345         if (localBridge == null) {
346             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
347                     "Bridge is missing or offline, you need to setup a working MQTT broker first.");
348             return;
349         }
350         ThingUID thingUID = localBridge.getUID();
351         Thing thing = thingRegistry.get(thingUID);
352         if (thing == null) {
353             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
354                     "Bridge is missing or offline, you need to setup a working MQTT broker first.");
355             return;
356         }
357         ThingHandler handler = thing.getHandler();
358         if (handler instanceof AbstractBrokerHandler) {
359             AbstractBrokerHandler abh = (AbstractBrokerHandler) handler;
360             MqttBrokerConnection localConnection = abh.getConnection();
361             if (localConnection != null) {
362                 localConnection.setKeepAliveInterval(20);
363                 localConnection.setQos(1);
364                 localConnection.setUnsubscribeOnStop(true);
365                 localConnection.addConnectionObserver(this);
366                 localConnection.start();
367                 localConnection.subscribe(fullStatesTopic + "/#", this);
368                 connection = localConnection;
369                 if (localConnection.connectionState().compareTo(MqttConnectionState.CONNECTED) == 0) {
370                     updateStatus(ThingStatus.ONLINE);
371                 }
372             }
373         }
374         return;
375     }
376
377     @Override
378     public void dispose() {
379         MqttBrokerConnection localConnection = connection;
380         if (localConnection != null) {
381             localConnection.unsubscribe(fullStatesTopic + "/#", this);
382         }
383     }
384 }