2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
14 package org.openhab.binding.mqtt.espmilighthub.internal.handler;
16 import static org.openhab.binding.mqtt.MqttBindingConstants.BINDING_ID;
17 import static org.openhab.binding.mqtt.espmilighthub.internal.EspMilightHubBindingConstants.*;
19 import java.math.BigDecimal;
20 import java.nio.charset.StandardCharsets;
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;
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.
55 * @author Matthew Skinner - Initial contribution
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();
73 public EspMilightHubHandler(Thing thing, ThingRegistry thingRegistry) {
75 this.thingRegistry = thingRegistry;
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)) {
83 // These two are 8 channel remotes
86 updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "5:" + channel),
88 updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "6:" + channel),
90 updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "7:" + channel),
92 updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "8:" + channel),
95 updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "1:" + channel),
97 updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "2:" + channel),
99 updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "3:" + channel),
101 updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "4:" + channel),
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;
117 tempBulbLevel = new BigDecimal(bulbLevel);
118 changeChannel(CHANNEL_LEVEL, new PercentType(tempBulbLevel));
120 } else if ("ON".equals(bulbState) || "OFF".equals(bulbState)) { // NOTE: Level is missing when this runs
121 changeChannel(CHANNEL_LEVEL, OnOffType.valueOf(bulbState));
123 bulbMode = Helper.resolveJSON(messageJSON, "\"bulb_mode\":\"", 5);
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"));
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));
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.");
145 if (bulbSaturation.isEmpty()) {
146 bulbSaturation = "100";
148 changeChannel(CHANNEL_COLOUR, new HSBType(bulbHue + "," + bulbSaturation + "," + tempBulbLevel));
152 if (!"cct".equals(globeType) && !"fut091".equals(globeType)) {
153 changeChannel(CHANNEL_BULB_MODE, new StringType("scene"));
155 String bulbDiscoMode = Helper.resolveJSON(messageJSON, "\"mode\":", 1);
156 if (!bulbDiscoMode.isEmpty()) {
157 changeChannel(CHANNEL_DISCO_MODE, new StringType(bulbDiscoMode));
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"));
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
175 private int autoColourTemp(int brightness) {
176 return minColourTemp.subtract(
177 minColourTemp.subtract(maxColourTemp).divide(BIG_DECIMAL_100).multiply(new BigDecimal(brightness)))
182 if (config.powerFailsToMinimum) {
183 sendMQTT("{\"state\":\"OFF\",\"level\":0}");
185 sendMQTT("{\"state\":\"OFF\"}");
189 void handleLevelColour(Command command) {
190 if (command instanceof OnOffType) {
191 if (OnOffType.ON.equals(command)) {
192 sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel + "}");
197 } else if (command instanceof IncreaseDecreaseType) {
198 if (IncreaseDecreaseType.INCREASE.equals(command)) {
199 if (savedLevel.intValue() <= 90) {
200 savedLevel = savedLevel.add(BigDecimal.TEN);
203 if (savedLevel.intValue() >= 10) {
204 savedLevel = savedLevel.subtract(BigDecimal.TEN);
207 sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel.intValue() + "}");
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\"}");
219 } else if (PercentType.ZERO.equals(hsb.getBrightness())) {
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() + "}");
226 sendMQTT("{\"state\":\"ON\",\"level\":" + hsb.getBrightness().intValue() + ",\"hue\":"
227 + hsb.getHue().intValue() + ",\"saturation\":" + hsb.getSaturation().intValue() + "}");
229 savedLevel = hsb.getBrightness().toBigDecimal();
231 } else if (command instanceof PercentType) {
232 PercentType percentType = (PercentType) command;
233 if (percentType.intValue() == 0) {
236 } else if (percentType.intValue() == 1 && config.oneTriggersNightMode) {
237 sendMQTT("{\"command\":\"night_mode\"}");
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()) + "}");
252 public void handleCommand(ChannelUID channelUID, Command command) {
253 if (command instanceof RefreshType) {
256 switch (channelUID.getId()) {
258 handleLevelColour(command);
260 case CHANNEL_BULB_MODE:
261 bulbMode = command.toString();
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 + "}");
267 case CHANNEL_COMMAND:
268 sendMQTT("{\"command\":\"" + command + "\"}");
270 case CHANNEL_DISCO_MODE:
271 sendMQTT("{\"mode\":\"" + command + "\"}");
274 handleLevelColour(command);
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.");
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.");
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())) {
306 "The milight globe {}{} is using lowercase for the remote code when the hub needs UPPERCASE",
307 remotesIDCode, remotesGroupID);
309 channelPrefix = BINDING_ID + ":" + globeType + ":" + localBridge.getUID().getId() + ":" + remotesIDCode
310 + remotesGroupID + ":";
315 private void sendMQTT(String payload) {
316 MqttBrokerConnection localConnection = connection;
317 if (localConnection != null) {
318 localConnection.publish(fullCommandTopic, payload.getBytes(), 1, false);
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);
330 public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
331 logger.debug("MQTT brokers state changed to:{}", state);
334 updateStatus(ThingStatus.ONLINE);
338 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
339 "Bridge (broker) is not connected to your MQTT broker.");
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.");
350 ThingUID thingUID = localBridge.getUID();
351 Thing thing = thingRegistry.get(thingUID);
353 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
354 "Bridge is missing or offline, you need to setup a working MQTT broker first.");
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);
378 public void dispose() {
379 MqttBrokerConnection localConnection = connection;
380 if (localConnection != null) {
381 localConnection.unsubscribe(fullStatesTopic + "/#", this);