diff --git a/CODEOWNERS b/CODEOWNERS index c4aa2073cdd3e..ace489c4bc9c3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -164,6 +164,7 @@ /bundles/org.openhab.binding.monopriceaudio/ @mlobstein /bundles/org.openhab.binding.mpd/ @stefanroellin /bundles/org.openhab.binding.mqtt/ @davidgraeff +/bundles/org.openhab.binding.mqtt.espmilighthub/ @Skinah /bundles/org.openhab.binding.mqtt.generic/ @davidgraeff /bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff /bundles/org.openhab.binding.mqtt.homie/ @davidgraeff diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 95becddb6ca41..78f2ff07eb109 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -806,6 +806,11 @@ org.openhab.binding.mqtt ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.mqtt.espmilighthub + ${project.version} + org.openhab.addons.bundles org.openhab.binding.mqtt.generic diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/NOTICE b/bundles/org.openhab.binding.mqtt.espmilighthub/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/README.md b/bundles/org.openhab.binding.mqtt.espmilighthub/README.md new file mode 100644 index 0000000000000..58a76b6c988e1 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/README.md @@ -0,0 +1,193 @@ +# EspMilightHub Binding + +This binding allows an open source esp8266 based bridge to automatically find and add Milight globes. +The hubs can be built from 2 ready made boards and only need connecting with 7 wires. +They can be very easy to build with no soldering needed. + +Advantages to using this DIY bridge over the OEM bridge: + ++ Almost unlimited groups to give individual control over an entire house of Milight globes without needing multiple bridges. ++ If using the Milight remotes to control the globes, this binding can update the openHAB controls the moment a key is pressed on the physical remotes. ++ Supports auto discovery. + +## Setup the hardware + +In depth details on how to build and what the bridge is can be found here: + +A quick overview of the steps to get the hardware going are: + ++ Connect a nodemcu/D1 mini/esp8266 to your computer via a USB cable. ++ Download the latest BIN file from here ++ Download esp8266flasher if you are on windows ++ Check the blog above on more info for Mac or Linux. ++ Open the flasher tool and make sure the flash size is 4mb or whatever your esp8266 board has. ++ Flash the bin and press the reset button on the board when it completes. ++ Connect to the wifi access point of the esp directly with your phone/tablet and setup wifi details. ++ Login by using the IP address of the esp8266 in a web browser and the control panel will show up. ++ Connect 7 wires between the two ready made PCBs as shown in the blog. ++ Setup a MQTT broker as this method uses the faster and lightweight MQTT protocol and not UDP. + +## Setup the Firmware + +Enter the control panel for the ESP8266 by using any browser and enter the IP address. +The following options need to be changed in the firmware for the binding to work. +Click on SETTINGS>MQTT>: + +**mqtt_topic_pattern:** +`milight/commands/:device_id/:device_type/:group_id` + +**mqtt_update_topic_pattern:** +Leave this blank. + +**mqtt_state_topic_pattern:** +`milight/states/:device_id/:device_type/:group_id` + +**group_state_fields:** +IMPORTANT: Make sure only the following are ticked: + ++ state ++ level ++ hue ++ saturation ++ mode ++ color_temp ++ bulb_mode + +Fill in the MQTT broker fields with the correct details so the hub can connect and then click **save**. +Now when you use any Milight remote control, you will see MQTT topics being created that should include `level` and `hsb` in the messages. +If you see `brightness` and not `level`, then go back and follow the above setup steps. + +You can use this Linux command to watch all MQTT topics from Milight: + +``` +mosquitto_sub -u usernamehere -P passwordhere -p 1883 -v -t 'milight/#' +``` + +You can also use the mosquitto_pub command to send your own commands and watch the bulbs respond all without the binding being setup. +Everything this binding does goes in and out via MQTT and can be watched with the above command. +Once you have setup and test the hub you can move onto using the binding. + +## Supported Things + +This binding is best thought of as a remote control emulator, so the things are really the type of remote that you own and not the globes. +The Milight protocol is 1 way only so there is no way to find actual globes. + +| Thing Type ID | Description | +|-|-| +| `rgb_cct` | Remote that has 4 channels and controls globes with full colour, and both cool and warm whites. | +| `fut089` | Remote is the newer 8 channel type called FUT089 and your globes are the rgb_cct. | +| `cct` | Remote is 4 channels and the globes have no colours with only cool and warm white controls. | +| `fut091` | Remote is the newer 8 group model called a fut091 and your globes are cct. | +| `rgbw` | Remote is 4 channels and the globes have RGB and a fixed white. | +| `rgb` | Remote is 4 channels and the globes have full RGB with no white. | + +## Discovery + +First install the MQTT binding and setup a `broker` thing and make sure it is ONLINE, as this binding uses the MQTT binding to talk to your broker and hence that binding must be setup first. +Next, move a control on either a physical remote, or used a virtual control inside the esp8266 control panel web page which cause a MQTT message to be sent. +This binding should then detect the new device the moment the control is moved and a new entry should appear in your INBOX. + +To remove a saved state from your MQTT broker that causes an entry in your INBOX you can use this command or use the ignore feature of openHAB. + +``` +mosquitto_pub -u username -P password -p 1883 -t 'milight/states/0x0/rgb_cct/1' -n -r +``` + +## Thing Configuration + +| Parameter | Description | Required | Default | +|-|-|-|-| +| `whiteHue` | When both the `whiteHue` and `whiteSat` values are seen by the binding it will trigger the white LEDS. Set to -1 to disable, 0 for Alexa, or 35 for Google Home. | Y | 35 | +| `whiteSat` | When both the whiteHue and whiteSat values are seen by the binding it will trigger the white LEDS. Set to -1 to disable, 100 for Alexa or 32 for Google Home. | Y | 32 | +| `favouriteWhite` | When one of the shortcuts triggers white mode, use this for the colour white instead of the default colour. | Y |200 | +| `dimmedCT` | Traditional globes grow warmer the more they are dimmed. Set this to 370, or leave blank to disable. | N | blank | +| `oneTriggersNightMode` | Night mode is a much lower level of light and this feature allows it to be auto selected when your fader/slider moves to 1%. NOTE: Night mode by design locks out some controls of a physical remote, so this feature is disabled by default. | Y | false | +| `powerFailsToMinimum` | If lights loose power from the power switch OR a power outage, they will default to using the lowest brightness if the light was turned off before the power failure occurred. | Y | true | +| `whiteThreshold` | RGBW globes do not respond to saturation changes, so this feature allows you to specify a number that if the saturation drops below, it will trigger the white mode. -1 will disable this feature. | Y | 12 | + +## Channels + +| Channel | Type | Description | +|-|-|-| +| `level` | Dimmer | Level changes the brightness of the globe. | +| `colourTemperature` | Dimmer | Change from cool to warm white with this control. | +| `colour` | Color | Allows you to change the colour, brightness and saturation of the globe. | +| `discoMode` | String | Switch to a Disco mode directly from a drop down list. | +| `bulbMode` | String (read only) | Displays the mode the bulb is currently in so that rules can determine if the globe is white, a color, disco modes or night mode are selected. | +| `command` | String | Sends the raw commands that the buttons on a remote send. | + +## Note Regarding Transmission Delays + +If you have lots of globes and openHAB turns them all on, you may notice a delay that causes the globes to turn on one by one and the delay can add up when a lot of globes are installed in your house. +This is caused by the time it takes to transmit the desired setting to the globe multiplied by how many times the hub repeats transmitting the setting. +Since it takes around 2.8ms for a setting to be transmitted, if the firmware is set to repeat the packets 50 times it would then take 2.8*50 = 140ms before the next globe starts to have its new state transmitted by the hub. +You can reduce the packet repeats to speed up the response of this binding and the hub by tweaking a few settings. + +Settings can be found on the radio tab in the esp control panel using your browser. +Suggested settings are as follows: + ++ Packet repeats = 12 (if you only turn 1 globe on or off it uses this value) ++ Packet repeat throttle threshold = 200 ++ Packet repeat throttle sensitivity = 0 ++ Packet repeat minimum = 8 (When turning multiple globes on and off it will use this value as it throttles the repeats back to reduce latency/delay between each globe) + +## Important for Textual Configuration + +This binding requires things to have a specific format for the unique ID, the auto discovery does this for you. + +If doing textual configuration you need to add the Device ID and Group ID together to create the things unique ID. +The DeviceID is different for each remote. +The GroupID can be 0 (all channels on the remote), or 1 to 8 for each of the individual channels on the remote). +If you do not understand this please use auto discovery to do it for you. + +The formula is +DeviceID + GroupID = ThingUID + +For example: + +| Device ID | Group ID |ThingUID | +|-----------|----------|----------| +| 0xE6C | 4 | 0xE6C4 | +| 0xB4CA | 4 | 0xB4CA4 | +| 0xB4CA | 8 | 0xB4CA8 | +| 0xB4CA | 0 | 0xB4CA0 | + +## Full Example + +To use these examples for textual configuration, you must already have a configured a MQTT `broker` thing and know its unique ID. +This UID will be used in the things file and will replace the text `myBroker`. +The first line in the things file will create a `broker` thing and this can be removed if you have already setup a broker in another file or via the UI already. + +*.things + +``` +Bridge mqtt:broker:myBroker [ host="localhost", secure=false, password="*******", qos=1, username="user"] +Thing mqtt:rgb_cct:0xE6C4 "Hallway" (mqtt:broker:myBroker) @ "MQTT" +``` + +*.items + +``` +Dimmer Hallway_Level "Front Hall" {channel="mqtt:rgb_cct:0xE6C4:level"} +Dimmer Hallway_ColourTemperature "White Color Temp" {channel="mqtt:rgb_cct:0xE6C4:colourTemperature"} +Color Hallway_Colour "Front Hall" ["Lighting"] {channel="mqtt:rgb_cct:0xE6C4:colour"} +String Hallway_DiscoMode "Disco Mode" {channel="mqtt:rgb_cct:0xE6C4:discoMode"} +String Hallway_BulbCommand "Send Command" {channel="mqtt:rgb_cct:0xE6C4:command"} +String Hallway_BulbMode "Bulb Mode" {channel="mqtt:rgb_cct:0xE6C4:bulbMode"} + +``` + +*.sitemap + +``` + Text label="Hallway" icon="light" + { + Switch item=Hallway_Level + Slider item=Hallway_Level + Slider item=Hallway_ColourTemperature + Colorpicker item=Hallway_Colour + Selection item=Hallway_DiscoMode + Text item=Hallway_BulbMode + Switch item=Hallway_BulbCommand mappings=[next_mode='Mode +', previous_mode='Mode -', mode_speed_up='Speed +', mode_speed_down='Speed -', set_white='White', night_mode='Night' ] + } +``` diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/pom.xml b/bundles/org.openhab.binding.mqtt.espmilighthub/pom.xml new file mode 100644 index 0000000000000..9c0e234649af3 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/pom.xml @@ -0,0 +1,24 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.mqtt.espmilighthub + openHAB Add-ons :: Bundles :: MQTT EspMilightHub + + + + org.openhab.addons.bundles + org.openhab.binding.mqtt + ${project.version} + provided + + + diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/feature/feature.xml b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/feature/feature.xml new file mode 100644 index 0000000000000..1f8b3a2cd5052 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/feature/feature.xml @@ -0,0 +1,12 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-mqtt + mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.espmilighthub/${project.version} + + + diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/ConfigOptions.java b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/ConfigOptions.java new file mode 100644 index 0000000000000..138fa3ae1f829 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/ConfigOptions.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.espmilighthub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ConfigOptions} Holds the config for the settings. + * + * @author Matthew Skinner - Initial contribution + */ +@NonNullByDefault +public class ConfigOptions { + public int whiteThreshold = -1; + public int whiteSat = 32; + public int whiteHue = 35; + public int favouriteWhite = 200; + public boolean oneTriggersNightMode = false; + public boolean powerFailsToMinimum = false; + public int dimmedCT = -1; +} diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/EspMilightHubBindingConstants.java b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/EspMilightHubBindingConstants.java new file mode 100644 index 0000000000000..b62cb29642e49 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/EspMilightHubBindingConstants.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.mqtt.espmilighthub.internal; + +import static org.openhab.binding.mqtt.MqttBindingConstants.BINDING_ID; + +import java.math.BigDecimal; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link EspMilightHubBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Matthew Skinner - Initial contribution + */ +@NonNullByDefault +public class EspMilightHubBindingConstants { + public static final String STATES_BASE_TOPIC = "milight/states/"; + public static final String COMMANDS_BASE_TOPIC = "milight/commands/"; + public static final BigDecimal BIG_DECIMAL_100 = new BigDecimal(100); + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_RGB_CCT = new ThingTypeUID(BINDING_ID, "rgb_cct"); + public static final ThingTypeUID THING_TYPE_CCT = new ThingTypeUID(BINDING_ID, "cct"); + public static final ThingTypeUID THING_TYPE_RGBW = new ThingTypeUID(BINDING_ID, "rgbw"); + public static final ThingTypeUID THING_TYPE_RGB = new ThingTypeUID(BINDING_ID, "rgb"); + public static final ThingTypeUID THING_TYPE_FUT089 = new ThingTypeUID(BINDING_ID, "fut089"); + public static final ThingTypeUID THING_TYPE_FUT091 = new ThingTypeUID(BINDING_ID, "fut091"); + + public static final Set SUPPORTED_THING_TYPES = Set.of(THING_TYPE_RGBW, THING_TYPE_RGB_CCT, + THING_TYPE_FUT089, THING_TYPE_FUT091, THING_TYPE_CCT, THING_TYPE_RGB); + + // Channels + public static final String CHANNEL_LEVEL = "level"; + public static final String CHANNEL_COLOUR = "colour"; + public static final String CHANNEL_COLOURTEMP = "colourTemperature"; + public static final String CHANNEL_DISCO_MODE = "discoMode"; + public static final String CHANNEL_BULB_MODE = "bulbMode"; + public static final String CHANNEL_COMMAND = "command"; +} diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/EspMilightHubHandlerFactory.java b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/EspMilightHubHandlerFactory.java new file mode 100644 index 0000000000000..4817d0ae5127c --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/EspMilightHubHandlerFactory.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.mqtt.espmilighthub.internal; + +import static org.openhab.binding.mqtt.espmilighthub.internal.EspMilightHubBindingConstants.SUPPORTED_THING_TYPES; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mqtt.espmilighthub.internal.handler.EspMilightHubHandler; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link EspMilightHubHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Matthew Skinner - Initial contribution + */ +@Component(service = ThingHandlerFactory.class) +@NonNullByDefault +public class EspMilightHubHandlerFactory extends BaseThingHandlerFactory { + private final ThingRegistry thingRegistry; + + @Activate + public EspMilightHubHandlerFactory(final @Reference ThingRegistry thingRegistry) { + this.thingRegistry = thingRegistry; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) { + return new EspMilightHubHandler(thing, thingRegistry); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/Helper.java b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/Helper.java new file mode 100644 index 0000000000000..5663a52fbf675 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/Helper.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.espmilighthub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link Helper} Removes the need for any external JSON libs + * + * @author Matthew Skinner - Initial contribution + */ +@NonNullByDefault +public class Helper { + /** + * resolveJSON will return a value from any key/path that you give and the string can be terminated by any ,}" + * characters. + * + */ + public static String resolveJSON(String messageJSON, String jsonPath, int resultMaxLength) { + String result = ""; + int index = 0; + index = messageJSON.indexOf(jsonPath); + if (index != -1) { + if ((index + jsonPath.length() + resultMaxLength) > messageJSON.length()) { + result = (messageJSON.substring(index + jsonPath.length(), messageJSON.length())); + } else { + result = (messageJSON.substring(index + jsonPath.length(), + index + jsonPath.length() + resultMaxLength)); + } + index = result.indexOf(','); + if (index == -1) { + index = result.indexOf('"'); + if (index == -1) { + index = result.indexOf('}'); + if (index == -1) { + return result; + } else { + return result.substring(0, index); + } + } else { + return result.substring(0, index); + } + } else { + result = result.substring(0, index); + index = result.indexOf('"'); + if (index == -1) { + return result; + } else { + return result.substring(0, index); + } + } + } + return ""; + } +} diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/discovery/EspMilightHubDiscoveryService.java b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/discovery/EspMilightHubDiscoveryService.java new file mode 100644 index 0000000000000..b855ef4c37d3c --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/discovery/EspMilightHubDiscoveryService.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.mqtt.espmilighthub.internal.discovery; + +import static org.openhab.binding.mqtt.MqttBindingConstants.BINDING_ID; +import static org.openhab.binding.mqtt.espmilighthub.internal.EspMilightHubBindingConstants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mqtt.discovery.AbstractMQTTDiscovery; +import org.openhab.binding.mqtt.discovery.MQTTTopicDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link EspMilightHubDiscoveryService} is responsible for finding globes + * and setting them up for the handlers. + * + * @author Matthew Skinner - Initial contribution + */ + +@Component(service = DiscoveryService.class, configurationPid = "discovery.mqttespmilighthub") +@NonNullByDefault +public class EspMilightHubDiscoveryService extends AbstractMQTTDiscovery { + protected final MQTTTopicDiscoveryService discoveryService; + + @Activate + public EspMilightHubDiscoveryService(@Reference MQTTTopicDiscoveryService discoveryService) { + super(SUPPORTED_THING_TYPES, 3, true, STATES_BASE_TOPIC + "#"); + this.discoveryService = discoveryService; + } + + @Override + protected MQTTTopicDiscoveryService getDiscoveryService() { + return discoveryService; + } + + @Override + public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection connection, String topic, + byte[] payload) { + resetTimeout(); + if (topic.startsWith(STATES_BASE_TOPIC)) { + String cutTopic = topic.replace(STATES_BASE_TOPIC, ""); + int index = cutTopic.indexOf("/"); + if (index != -1) // -1 means "not found" + { + String remoteCode = (cutTopic.substring(0, index)); // Store the remote code for use later + cutTopic = topic.replace(STATES_BASE_TOPIC + remoteCode + "/", ""); + index = cutTopic.indexOf("/"); + if (index != -1) { + String globeType = (cutTopic.substring(0, index)); + String remoteGroupID = (cutTopic.substring(index + 1, index + 2)); + // openHAB's framework has better code for handling groups then the firmware does + if (!remoteGroupID.equals("0")) {// Users can manually add group 0 things if they wish + publishDevice(connectionBridge, connection, topic, remoteCode, globeType, remoteGroupID); + } + } + } + } + } + + void publishDevice(ThingUID connectionBridge, MqttBrokerConnection connection, String topic, String remoteCode, + String globeType, String remoteGroupID) { + Map properties = new HashMap<>(); + properties.put("deviceid", remoteCode + remoteGroupID); + properties.put("basetopic", STATES_BASE_TOPIC + remoteCode + "/" + globeType + "/" + remoteGroupID); + thingDiscovered(DiscoveryResultBuilder + .create(new ThingUID(new ThingTypeUID(BINDING_ID, globeType), connectionBridge, + remoteCode + remoteGroupID)) + .withProperties(properties).withRepresentationProperty("deviceid").withBridge(connectionBridge) + .withLabel("Milight " + globeType).build()); + } + + @Override + public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connection, String topic) { + } +} diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/handler/EspMilightHubHandler.java b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/handler/EspMilightHubHandler.java new file mode 100644 index 0000000000000..6182f79266451 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/java/org/openhab/binding/mqtt/espmilighthub/internal/handler/EspMilightHubHandler.java @@ -0,0 +1,387 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.mqtt.espmilighthub.internal.handler; + +import static org.openhab.binding.mqtt.MqttBindingConstants.BINDING_ID; +import static org.openhab.binding.mqtt.espmilighthub.internal.EspMilightHubBindingConstants.*; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mqtt.espmilighthub.internal.ConfigOptions; +import org.openhab.binding.mqtt.espmilighthub.internal.Helper; +import org.openhab.binding.mqtt.handler.AbstractBrokerHandler; +import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; +import org.openhab.core.io.transport.mqtt.MqttConnectionState; +import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EspMilightHubHandler} is responsible for handling commands of the globes, which are then + * sent to one of the bridges to be sent out by MQTT. + * + * @author Matthew Skinner - Initial contribution + */ +@NonNullByDefault +public class EspMilightHubHandler extends BaseThingHandler implements MqttConnectionObserver, MqttMessageSubscriber { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private @Nullable MqttBrokerConnection connection; + private ThingRegistry thingRegistry; + private String globeType = ""; + private String bulbMode = ""; + private String remotesGroupID = ""; + private String channelPrefix = ""; + private String fullCommandTopic = ""; + private String fullStatesTopic = ""; + private BigDecimal maxColourTemp = BigDecimal.ZERO; + private BigDecimal minColourTemp = BigDecimal.ZERO; + private BigDecimal savedLevel = BIG_DECIMAL_100; + private ConfigOptions config = new ConfigOptions(); + + public EspMilightHubHandler(Thing thing, ThingRegistry thingRegistry) { + super(thing); + this.thingRegistry = thingRegistry; + } + + void changeChannel(String channel, State state) { + updateState(new ChannelUID(channelPrefix + channel), state); + // Remote code of 0 means that all channels need to follow this change. + if (remotesGroupID.equals("0")) { + switch (globeType) { + // These two are 8 channel remotes + case "fut091": + case "fut089": + updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "5:" + channel), + state); + updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "6:" + channel), + state); + updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "7:" + channel), + state); + updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "8:" + channel), + state); + default: + updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "1:" + channel), + state); + updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "2:" + channel), + state); + updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "3:" + channel), + state); + updateState(new ChannelUID(channelPrefix.substring(0, channelPrefix.length() - 2) + "4:" + channel), + state); + } + } + } + + private void processIncomingState(String messageJSON) { + // Need to handle State and Level at the same time to process level=0 as off// + BigDecimal tempBulbLevel = BigDecimal.ZERO; + String bulbState = Helper.resolveJSON(messageJSON, "\"state\":\"", 3); + String bulbLevel = Helper.resolveJSON(messageJSON, "\"level\":", 3); + if (!bulbLevel.isEmpty()) { + if (bulbLevel.equals("0") || bulbState.equals("OFF")) { + changeChannel(CHANNEL_LEVEL, OnOffType.OFF); + tempBulbLevel = BigDecimal.ZERO; + } else { + tempBulbLevel = new BigDecimal(bulbLevel); + changeChannel(CHANNEL_LEVEL, new PercentType(tempBulbLevel)); + } + } else if (bulbState.equals("ON") || bulbState.equals("OFF")) { // NOTE: Level is missing when this runs + changeChannel(CHANNEL_LEVEL, OnOffType.valueOf(bulbState)); + } + bulbMode = Helper.resolveJSON(messageJSON, "\"bulb_mode\":\"", 5); + switch (bulbMode) { + case "white": + if (!globeType.equals("cct") && !globeType.equals("fut091")) { + changeChannel(CHANNEL_BULB_MODE, new StringType("white")); + changeChannel(CHANNEL_COLOUR, new HSBType("0,0," + tempBulbLevel)); + changeChannel(CHANNEL_DISCO_MODE, new StringType("None")); + } + String bulbCTemp = Helper.resolveJSON(messageJSON, "\"color_temp\":", 3); + if (!bulbCTemp.isEmpty()) { + int ibulbCTemp = (int) Math.round(((Float.valueOf(bulbCTemp) / 2.17) - 171) * -1); + changeChannel(CHANNEL_COLOURTEMP, new PercentType(ibulbCTemp)); + } + break; + case "color": + changeChannel(CHANNEL_BULB_MODE, new StringType("color")); + changeChannel(CHANNEL_DISCO_MODE, new StringType("None")); + String bulbHue = Helper.resolveJSON(messageJSON, "\"hue\":", 3); + String bulbSaturation = Helper.resolveJSON(messageJSON, "\"saturation\":", 3); + if (bulbHue.isEmpty()) { + logger.warn("Milight MQTT message came in as being a colour mode, but was missing a HUE value."); + } else { + if (bulbSaturation.isEmpty()) { + bulbSaturation = "100"; + } + changeChannel(CHANNEL_COLOUR, new HSBType(bulbHue + "," + bulbSaturation + "," + tempBulbLevel)); + } + break; + case "scene": + if (!globeType.equals("cct") && !globeType.equals("fut091")) { + changeChannel(CHANNEL_BULB_MODE, new StringType("scene")); + } + String bulbDiscoMode = Helper.resolveJSON(messageJSON, "\"mode\":", 1); + if (!bulbDiscoMode.isEmpty()) { + changeChannel(CHANNEL_DISCO_MODE, new StringType(bulbDiscoMode)); + } + break; + case "night": + if (!globeType.equals("cct") && !globeType.equals("fut091")) { + changeChannel(CHANNEL_BULB_MODE, new StringType("night")); + if (config.oneTriggersNightMode) { + changeChannel(CHANNEL_LEVEL, new PercentType("1")); + } + } + break; + } + } + + /* + * Used to calculate the colour temp for a globe if you want the light to get warmer as it is dimmed like a + * traditional halogen globe + */ + private int autoColourTemp(int brightness) { + return minColourTemp.subtract( + minColourTemp.subtract(maxColourTemp).divide(BIG_DECIMAL_100).multiply(new BigDecimal(brightness))) + .intValue(); + } + + void turnOff() { + if (config.powerFailsToMinimum) { + sendMQTT("{\"state\":\"OFF\",\"level\":0}"); + } else { + sendMQTT("{\"state\":\"OFF\"}"); + } + } + + void handleLevelColour(Command command) { + if (command instanceof OnOffType) { + if (OnOffType.ON.equals(command)) { + sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel + "}"); + return; + } else { + turnOff(); + } + } else if (command instanceof IncreaseDecreaseType) { + if (IncreaseDecreaseType.INCREASE.equals(command)) { + if (savedLevel.intValue() <= 90) { + savedLevel = savedLevel.add(BigDecimal.TEN); + } + } else { + if (savedLevel.intValue() >= 10) { + savedLevel = savedLevel.subtract(BigDecimal.TEN); + } + } + sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel.intValue() + "}"); + return; + } else if (command instanceof HSBType) { + HSBType hsb = (HSBType) command; + // This feature allows google home or Echo to trigger white mode when asked to turn color to white. + if (hsb.getHue().intValue() == config.whiteHue && hsb.getSaturation().intValue() == config.whiteSat) { + if ("rgb_cct".equals(globeType) || "fut089".equals(globeType)) { + sendMQTT("{\"state\":\"ON\",\"color_temp\":" + config.favouriteWhite + "}"); + } else {// globe must only have 1 type of white + sendMQTT("{\"command\":\"set_white\"}"); + } + return; + } else if (PercentType.ZERO.equals(hsb.getBrightness())) { + turnOff(); + return; + } else if (config.whiteThreshold != -1 && hsb.getSaturation().intValue() <= config.whiteThreshold + && "rgbw".equals(globeType)) { + sendMQTT("{\"command\":\"set_white\"}"); + return; + } + sendMQTT("{\"state\":\"ON\",\"level\":" + hsb.getBrightness().intValue() + ",\"hue\":" + + hsb.getHue().intValue() + ",\"saturation\":" + hsb.getSaturation().intValue() + "}"); + savedLevel = hsb.getBrightness().toBigDecimal(); + return; + } else if (command instanceof PercentType) { + PercentType percentType = (PercentType) command; + if (percentType.intValue() == 0) { + turnOff(); + return; + } else if (percentType.intValue() == 1 && config.oneTriggersNightMode) { + sendMQTT("{\"command\":\"night_mode\"}"); + return; + } + sendMQTT("{\"state\":\"ON\",\"level\":" + command + "}"); + savedLevel = percentType.toBigDecimal(); + if (globeType.equals("rgb_cct") || globeType.equals("fut089")) { + if (config.dimmedCT > 0 && bulbMode.equals("white")) { + sendMQTT("{\"state\":\"ON\",\"color_temp\":" + autoColourTemp(savedLevel.intValue()) + "}"); + } + } + return; + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + return; + } + switch (channelUID.getId()) { + case CHANNEL_LEVEL: + handleLevelColour(command); + return; + case CHANNEL_BULB_MODE: + bulbMode = command.toString(); + break; + case CHANNEL_COLOURTEMP: + int scaledCommand = (int) Math.round((370 - (2.17 * Float.valueOf(command.toString())))); + sendMQTT("{\"state\":\"ON\",\"level\":" + savedLevel + ",\"color_temp\":" + scaledCommand + "}"); + break; + case CHANNEL_COMMAND: + sendMQTT("{\"command\":\"" + command + "\"}"); + break; + case CHANNEL_DISCO_MODE: + sendMQTT("{\"mode\":\"" + command + "\"}"); + break; + case CHANNEL_COLOUR: + handleLevelColour(command); + } + } + + @Override + public void initialize() { + config = getConfigAs(ConfigOptions.class); + if (config.dimmedCT > 0) { + maxColourTemp = new BigDecimal(config.favouriteWhite); + minColourTemp = new BigDecimal(config.dimmedCT); + if (minColourTemp.intValue() <= maxColourTemp.intValue()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "The dimmedCT config value must be greater than the favourite White value."); + return; + } + } + Bridge localBridge = getBridge(); + if (localBridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, + "Globe must have a valid bridge selected before it can come online."); + return; + } else { + globeType = thing.getThingTypeUID().getId();// eg rgb_cct + String globeLocation = this.getThing().getUID().getId();// eg 0x014 + remotesGroupID = globeLocation.substring(globeLocation.length() - 1, globeLocation.length());// eg 4 + String remotesIDCode = globeLocation.substring(0, globeLocation.length() - 1);// eg 0x01 + fullCommandTopic = COMMANDS_BASE_TOPIC + remotesIDCode + "/" + globeType + "/" + remotesGroupID; + fullStatesTopic = STATES_BASE_TOPIC + remotesIDCode + "/" + globeType + "/" + remotesGroupID; + // Need to remove the lowercase x from 0x12AB in case it contains all numbers + String caseCheck = globeLocation.substring(2, globeLocation.length() - 1); + if (!caseCheck.equals(caseCheck.toUpperCase())) { + logger.warn( + "The milight globe {}{} is using lowercase for the remote code when the hub needs UPPERCASE", + remotesIDCode, remotesGroupID); + } + channelPrefix = BINDING_ID + ":" + globeType + ":" + localBridge.getUID().getId() + ":" + remotesIDCode + + remotesGroupID + ":"; + connectMQTT(); + } + } + + private void sendMQTT(String payload) { + MqttBrokerConnection localConnection = connection; + if (localConnection != null) { + localConnection.publish(fullCommandTopic, payload.getBytes(), 1, false); + } + } + + @Override + public void processMessage(String topic, byte[] payload) { + String state = new String(payload, StandardCharsets.UTF_8); + logger.trace("Recieved the following new Milight state:{}:{}", topic, state); + processIncomingState(state); + } + + @Override + public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) { + logger.debug("MQTT brokers state changed to:{}", state); + switch (state) { + case CONNECTED: + updateStatus(ThingStatus.ONLINE); + break; + case CONNECTING: + case DISCONNECTED: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Bridge (broker) is not connected to your MQTT broker."); + } + } + + public void connectMQTT() { + Bridge localBridge = this.getBridge(); + if (localBridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Bridge is missing or offline, you need to setup a working MQTT broker first."); + return; + } + ThingUID thingUID = localBridge.getBridgeUID(); + if (thingUID == null) { + return; + } + Thing thing = thingRegistry.get(thingUID); + if (thing == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Bridge is missing or offline, you need to setup a working MQTT broker first."); + return; + } + ThingHandler handler = thing.getHandler(); + if (handler instanceof AbstractBrokerHandler) { + AbstractBrokerHandler abh = (AbstractBrokerHandler) handler; + MqttBrokerConnection localConnection = abh.getConnection(); + if (localConnection != null) { + localConnection.setKeepAliveInterval(20); + localConnection.setQos(1); + localConnection.setUnsubscribeOnStop(true); + localConnection.addConnectionObserver(this); + localConnection.start(); + localConnection.subscribe(fullStatesTopic + "/#", this); + connection = localConnection; + if (localConnection.connectionState().compareTo(MqttConnectionState.CONNECTED) == 0) { + updateStatus(ThingStatus.ONLINE); + } + } + } + return; + } + + @Override + public void dispose() { + MqttBrokerConnection localConnection = connection; + if (localConnection != null) { + localConnection.unsubscribe(fullStatesTopic + "/#", this); + } + } +} diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..69be374fb168b --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,111 @@ + + + + + + + 1% on a slider will trigger the Night Mode. + false + + + + + If lights loose power when soft off, the lights will default back to the minimum brightness. + false + + + + + + + Traditional globes grow warmer the more they are dimmed. Set this to 370, or leave blank to disable. + + + + + + 1% on a slider will trigger the Night Mode. + false + + + + + + + When both the whiteHue and whiteSat values are seen by the binding it will trigger the white LEDS. + + 35 + + + + + When both the whiteHue and whiteSat values are seen by the binding it will trigger the white LEDS. + + 32 + + + + + When a shortcut triggers white mode, use this for the colour white. + 200 + + + + + Traditional globes grow warmer the more they are dimmed. Set this to 370, or leave blank to disable. + + + + + + 1% on a slider will trigger the Night Mode. + false + + + + + If lights loose power, the lights will turn on to the minimum brightness. + true + + + + + + + When both the whiteHue and whiteSat values are seen by the binding it will trigger the white LEDS. + + 35 + + + + + When both the whiteHue and whiteSat values are seen by the binding it will trigger the white LEDS. + + 32 + + + + + 1% on a slider will trigger the Night Mode. + false + + + + + If lights loose power, the lights will turn on to the minimum brightness. + false + + + + + RGBW saturation changes, will trigger the white mode. -1 will disable this feature. + + 12 + + + + + diff --git a/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..a7dd8a991170f --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.espmilighthub/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,190 @@ + + + + + + + + + + Led globe with full Colour, and both cool and warm whites. + Lightbulb + + + + + + + + + + + + + + + + + + Use this when your remote is the newer 8 group type called FUT089 and your globes are rgb_cct + Lightbulb + + + + + + + + + + + + + + + + + + Use this when your remote is the newer fut091 and your globes are cct + Lightbulb + + + + + + + + + + + + + + + Led globe with both cool and warm white controls + Lightbulb + + + + + + + + + + + + + + + RGB Globe with a fixed white + Lightbulb + + + + + + + + + + + + + + + + + RGB Globe with no white + Lightbulb + + + + + + + + + + + + Dimmer + + Level changes the brightness of the globe. + Slider + + + + Dimmer + + Change from cool to warm white with this control. + Slider + + + + Color + + Allows you to change the colour, brightness and saturation of the globe. + ColorLight + + Lighting + + + + + String + + Send a raw command to the globe/s. + + + + + + + + + + + + + + + + + + String + + Displays the mode the bulb is currently in. + + + + + + + + + + + + String + + Switch to a Disco mode directly. + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/MqttBindingConstants.java b/bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/MqttBindingConstants.java index 0ad0928f1b108..08d2579ac85db 100644 --- a/bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/MqttBindingConstants.java +++ b/bundles/org.openhab.binding.mqtt/src/main/java/org/openhab/binding/mqtt/MqttBindingConstants.java @@ -23,7 +23,7 @@ */ @NonNullByDefault public class MqttBindingConstants { - private static final String BINDING_ID = "mqtt"; + public static final String BINDING_ID = "mqtt"; // List of all Thing Type UIDs public static final ThingTypeUID BRIDGE_TYPE_SYSTEMBROKER = new ThingTypeUID(BINDING_ID, "systemBroker"); diff --git a/bundles/pom.xml b/bundles/pom.xml index 79efb9c54e682..8915314b72115 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -195,6 +195,7 @@ org.openhab.binding.monopriceaudio org.openhab.binding.mpd org.openhab.binding.mqtt + org.openhab.binding.mqtt.espmilighthub org.openhab.binding.mqtt.generic org.openhab.binding.mqtt.homeassistant org.openhab.binding.mqtt.homie diff --git a/features/openhab-addons/src/main/resources/footer.xml b/features/openhab-addons/src/main/resources/footer.xml index 0385fa97249df..291d0536502a1 100644 --- a/features/openhab-addons/src/main/resources/footer.xml +++ b/features/openhab-addons/src/main/resources/footer.xml @@ -20,6 +20,7 @@ openhab-runtime-base openhab-transport-mqtt mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version} + mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.espmilighthub/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.generic/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.homeassistant/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.homie/${project.version}