initial commit of lutron-mqtt (hacked wink hub) binding
27 files changed, 1960 insertions(+), 0 deletions(-)

A => .hgignore
A => ESH-INF/binding/binding.xml
A => ESH-INF/i18n/lutronmqtt_xx_XX.properties
A => ESH-INF/thing/hub.xml
A => ESH-INF/thing/thing-types.xml
A => META-INF/MANIFEST.MF
A => OSGI-INF/.gitignore
A => OSGI-INF/org.openhab.binding.lutronmqtt.discovery.LutronMQTTHubDiscoveryParticipant.xml
A => OSGI-INF/org.openhab.binding.lutronmqtt.internal.LutronMQTTHandlerFactory.xml
A => README.md
A => about.html
A => build.properties
A => pom.xml
A => src/main/java/org/openhab/binding/lutronmqtt/LutronMQTTBindingConstants.java
A => src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTDeviceDiscoveryService.java
A => src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTHubDiscoveryParticipant.java
A => src/main/java/org/openhab/binding/lutronmqtt/handler/DeviceStatusListener.java
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LightStateConverter.java
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTDimmableLightHandler.java
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTHubHandler.java
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTRemoteHandler.java
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTVariableFanHandler.java
A => src/main/java/org/openhab/binding/lutronmqtt/handler/PowerLevelDeviceHandler.java
A => src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTConfiguration.java
A => src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTHandlerFactory.java
A => src/main/java/org/openhab/binding/lutronmqtt/model/LutronDevice.java
A => src/main/java/org/openhab/binding/lutronmqtt/model/LutronDeviceState.java
A => .hgignore +5 -0
@@ 0,0 1,5 @@ 
+target
+.idea
+.classpath
+.project
+.iml$

          
A => ESH-INF/binding/binding.xml +11 -0
@@ 0,0 1,11 @@ 
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="lutronmqtt"
+				 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+				 xmlns:binding="http://eclipse.org/smarthome/schemas/binding/v1.0.0"
+				 xsi:schemaLocation="http://eclipse.org/smarthome/schemas/binding/v1.0.0 http://eclipse.org/smarthome/schemas/binding-1.0.0.xsd">
+
+	<name>LutronMQTT Binding</name>
+	<description>This is the binding for LutronMQTT.</description>
+	<author>William Welliver</author>
+
+</binding:binding>

          
A => ESH-INF/i18n/lutronmqtt_xx_XX.properties +13 -0
@@ 0,0 1,13 @@ 
+# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE
+# FIXME: please do not add the file to the repo if you add or change no content
+# binding
+binding.lutronmqtt.name = <Your localized Binding name>
+binding.lutronmqtt.description = <Your localized Binding description>
+
+# thing types
+thing-type.lutronmqtt.sample.label = <Your localized Thing label>
+thing-type.lutronmqtt.sample.description = <Your localized Thing description>
+
+# channel types
+channel-type.lutronmqtt.sample-channel.label = <Your localized Channel label>
+channel-type.lutronmqtt.sample-channel.description = <Your localized Channel description>
  No newline at end of file

          
A => ESH-INF/thing/hub.xml +28 -0
@@ 0,0 1,28 @@ 
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="lutronmqtt"
+                          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                          xmlns:thing="http://eclipse.org/smarthome/schemas/thing-description/v1.0.0"
+                          xsi:schemaLocation="http://eclipse.org/smarthome/schemas/thing-description/v1.0.0 http://eclipse.org/smarthome/schemas/thing-description-1.0.0.xsd">
+
+    <!-- Wink Hub -->
+    <bridge-type id="hub">
+        <label>Lutron-MQTT Gateway</label>
+        <description>The Lutron-MQTT Gateway represents the network device communicating with the Lutron ClearConnect network.</description>
+
+        <properties>
+            <property name="vendor">Hacienda</property>
+            <property name="uuid"></property>
+            <property name="url"></property>
+        </properties>
+
+        <config-description>
+            <parameter name="token" type="text">
+                <label>Security Token</label>
+                <description>The token authorized by the luton-mqtt infrastructure</description>
+                <required>true</required>
+            </parameter>
+        </config-description>
+
+    </bridge-type>
+
+</thing:thing-descriptions>
  No newline at end of file

          
A => ESH-INF/thing/thing-types.xml +96 -0
@@ 0,0 1,96 @@ 
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="lutronmqtt" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                          xmlns:thing="http://eclipse.org/smarthome/schemas/thing-description/v1.0.0"
+                          xsi:schemaLocation="http://eclipse.org/smarthome/schemas/thing-description/v1.0.0 http://eclipse.org/smarthome/schemas/thing-description-1.0.0.xsd">
+
+    <thing-type id="dimmableLight">
+        <supported-bridge-type-refs>
+            <bridge-type-ref id="hub"/>
+        </supported-bridge-type-refs>
+
+        <label>Dimmable Light</label>
+        <description>Controls dimmable loads</description>
+
+        <channels>
+            <channel id="lightlevel" typeId="lightDimmer"/>
+            <!--             <channel id="lightstate" typeId="lightSwitch"/>  -->
+        </channels>
+
+        <properties>
+            <property name="objectId"></property>
+            <property name="integrationId"></property>
+            <property name="name"></property>
+        </properties>
+    </thing-type>
+
+    <thing-type id="variableFan">
+        <supported-bridge-type-refs>
+            <bridge-type-ref id="hub"/>
+        </supported-bridge-type-refs>
+
+        <label>Variable Speed Fan</label>
+        <description>Controls variable speed fans</description>
+
+        <channels>
+            <channel id="fanSpeed" typeId="fanSpeed"/>
+            <!--             <channel id="powerSwitch" typeId="powerSwitch"/> -->
+        </channels>
+
+        <properties>
+            <property name="objectId"></property>
+            <property name="integrationId"></property>
+            <property name="linkAddress"></property>
+            <property name="name"></property>
+        </properties>
+    </thing-type>
+
+    <channel-type id="lightDimmer">
+        <item-type>Dimmer</item-type>
+        <label>Light Level</label>
+        <description>Increase/decrease the light level</description>
+        <category>DimmableLight</category>
+        <state min="0" max="100" pattern="%d %%"/>
+    </channel-type>
+
+    <channel-type id="lightSwitch">
+        <item-type>Switch</item-type>
+        <label>Light State</label>
+        <description>Turn the light on or off</description>
+        <category>DimmableLight</category>
+    </channel-type>
+
+    <channel-type id="fanSpeed">
+        <item-type>Dimmer</item-type>
+        <label>Fan Speed</label>
+        <description>Increase/decrease the speed</description>
+        <category>VariableFan</category>
+        <state min="0" max="100" pattern="%d %%"/>
+    </channel-type>
+
+    <channel-type id="powerSwitch">
+        <item-type>Switch</item-type>
+        <label>Power State</label>
+        <description>Turn the item on or off</description>
+        <category>VariableFan</category>
+    </channel-type>
+
+
+    <!-- Sample Thing Type -->
+    <thing-type id="sample">
+        <label>LutronMQTT Binding Thing</label>
+        <description>Sample thing for LutronMQTT Binding</description>
+
+        <channels>
+            <channel id="channel1" typeId="sample-channel" />
+        </channels>
+
+        <config-description>
+            <parameter name="config1" type="text" required="true">
+                <label>Sample parameter</label>
+                <description>This is a sample text configuration parameter.</description>
+            </parameter>
+        </config-description>
+
+    </thing-type>
+
+</thing:thing-descriptions>

          
A => META-INF/MANIFEST.MF +33 -0
@@ 0,0 1,33 @@ 
+Manifest-Version: 1.0
+Bundle-ActivationPolicy: lazy
+Bundle-ClassPath: .
+Bundle-ManifestVersion: 2
+Bundle-Name: LutronMQTT Binding
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Bundle-SymbolicName: org.openhab.binding.lutronmqtt;singleton:=true
+Bundle-Vendor: openHAB
+Bundle-Version: 2.3.0.qualifier
+Export-Package: 
+ org.openhab.binding.lutronmqtt,
+ org.openhab.binding.lutronmqtt.handler
+Import-Package: 
+ org.openhab.binding.lutronmqtt,
+ org.openhab.binding.lutronmqtt.handler,
+ org.eclipse.jdt.annotation;resolution:=optional,
+  javax.jmdns;version="3.5.0",
+ org.eclipse.smarthome.config.core,
+  org.eclipse.smarthome.config.discovery,
+  org.eclipse.smarthome.config.discovery.mdns,
+ org.eclipse.smarthome.core.library.types,
+ org.eclipse.smarthome.core.thing,
+ org.eclipse.smarthome.core.thing.binding,
+ org.eclipse.smarthome.core.thing.binding.builder,
+ org.eclipse.smarthome.core.thing.type,
+ org.eclipse.smarthome.core.types,
+ org.slf4j,
+ com.google.common.collect,
+ org.osgi.framework,
+ org.eclipse.paho.client.mqttv3,
+ org.eclipse.paho.client.mqttv3.persist,
+ com.google.gson
+Service-Component: OSGI-INF/*.xml

          
A => OSGI-INF/.gitignore +1 -0
@@ 0,0 1,1 @@ 
+*.xml
  No newline at end of file

          
A => OSGI-INF/org.openhab.binding.lutronmqtt.discovery.LutronMQTTHubDiscoveryParticipant.xml +7 -0
@@ 0,0 1,7 @@ 
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.2.0" immediate="true" name="org.openhab.binding.lutronmqtt.discovery.hub" configuration-pid="binding.lutronmqtt">
+    <service servicefactory="false">
+        <provide interface="org.eclipse.smarthome.config.discovery.mdns.MDNSDiscoveryParticipant"/>
+    </service>
+    <implementation class="org.openhab.binding.lutronmqtt.discovery.LutronMQTTHubDiscoveryParticipant"/>
+</scr:component>

          
A => OSGI-INF/org.openhab.binding.lutronmqtt.internal.LutronMQTTHandlerFactory.xml +7 -0
@@ 0,0 1,7 @@ 
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.2.0" immediate="true" name="lutronmqtt" configuration-pid="binding.lutronmqtt">
+    <service servicefactory="false">
+        <provide interface="org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory"/>
+    </service>
+    <implementation class="org.openhab.binding.lutronmqtt.internal.LutronMQTTHandlerFactory"/>
+</scr:component>

          
A => README.md +52 -0
@@ 0,0 1,52 @@ 
+# <bindingName> Binding
+
+_Give some details about what this binding is meant for - a protocol, system, specific device._
+
+_If possible, provide some resources like pictures, a YouTube video, etc. to give an impression of what can be done with this binding. You can place such resources into a `doc` folder next to this README.md._
+
+## Supported Things
+
+_Please describe the different supported things / devices within this section._
+_Which different types are supported, which models were tested etc.?_
+_Note that it is planned to generate some part of this based on the XML files within ```ESH-INF/thing``` of your binding._
+
+## Discovery
+
+_Describe the available auto-discovery features here. Mention for what it works and what needs to be kept in mind when using it._
+
+## Binding Configuration
+
+_If your binding requires or supports general configuration settings, please create a folder ```cfg``` and place the configuration file ```<bindingId>.cfg``` inside it. In this section, you should link to this file and provide some information about the options. The file could e.g. look like:_
+
+```
+# Configuration for the Philips Hue Binding
+#
+# Default secret key for the pairing of the Philips Hue Bridge.
+# It has to be between 10-40 (alphanumeric) characters 
+# This may be changed by the user for security reasons.
+secret=EclipseSmartHome
+```
+
+_Note that it is planned to generate some part of this based on the information that is available within ```ESH-INF/binding``` of your binding._
+
+_If your binding does not offer any generic configurations, you can remove this section completely._
+
+## Thing Configuration
+
+_Describe what is needed to manually configure a thing, either through the (Paper) UI or via a thing-file. This should be mainly about its mandatory and optional configuration parameters. A short example entry for a thing file can help!_
+
+_Note that it is planned to generate some part of this based on the XML files within ```ESH-INF/thing``` of your binding._
+
+## Channels
+
+_Here you should provide information about available channel types, what their meaning is and how they can be used._
+
+_Note that it is planned to generate some part of this based on the XML files within ```ESH-INF/thing``` of your binding._
+
+## Full Example
+
+_Provide a full usage example based on textual configuration files (*.things, *.items, *.sitemap)._
+
+## Any custom content here!
+
+_Feel free to add additional sections for whatever you think should also be mentioned about your binding!_

          
A => about.html +32 -0
@@ 0,0 1,32 @@ 
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
+    <title>About</title>
+</head>
+<body lang="EN-US">
+<h2>About This Content</h2>
+
+<p>March 30, 2017</p>
+<h3>License</h3>
+
+<p>
+    The openHAB community makes available all content in this plug-in (&quot;Content&quot;). Unless otherwise
+    indicated below, the Content is provided to you under the terms and conditions of the
+    Eclipse Public License Version 1.0 (&quot;EPL&quot;). A copy of the EPL is available
+    at <a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a>.
+    For purposes of the EPL, &quot;Program&quot; will mean the Content.
+</p>
+
+<p>
+    If you did not receive this Content directly from the openHAB community, the Content is
+    being redistributed by another party (&quot;Redistributor&quot;) and different terms and conditions may
+    apply to your use of any object code in the Content. Check the Redistributor's license that was
+    provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
+    indicated below, the terms and conditions of the EPL still apply to any source code in the Content
+    and such source code may be obtained at <a href="http://www.openhab.org/">openhab.org</a>.
+</p>
+
+</body>
+</html>

          
A => build.properties +6 -0
@@ 0,0 1,6 @@ 
+source..=src/main/java/
+output..=target/classes
+bin.includes=META-INF/,\
+             .,\
+             OSGI-INF/,\
+             ESH-INF/

          
A => pom.xml +166 -0
@@ 0,0 1,166 @@ 
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+	<modelVersion>4.0.0</modelVersion>
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.smarthome.config</groupId>
+            <artifactId>org.eclipse.smarthome.config.discovery.mdns</artifactId>
+            <version>0.9.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.paho</groupId>
+            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+            <version>1.0.2</version>
+        </dependency>
+    </dependencies>
+
+<parent>
+        <groupId>org.openhab</groupId>
+        <artifactId>pom-tycho</artifactId>
+        <version>2.4.0-SNAPSHOT</version>
+        <relativePath/>
+    </parent>
+
+        <properties>
+                <report.fail.on.error>false</report.fail.on.error>
+        <ohc.version>2.4.0-SNAPSHOT</ohc.version>
+    </properties>
+
+        <artifactId>org.openhab.binding.lutronmqtt</artifactId>
+
+        <name>LutronMQTT Binding</name>
+	<packaging>eclipse-plugin</packaging>
+
+    <build>
+        <testSourceDirectory>src/test</testSourceDirectory>
+        
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.19.1</version>
+                <executions>
+                    <execution>
+                        <id>test</id>
+                        <phase>test</phase>
+                        <configuration>
+                            <includes>
+                                <include>**/*Test.java</include>
+                            </includes>
+                        </configuration>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.5.1</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>compiletests</id>
+                        <phase>test-compile</phase>
+                        <goals>
+                            <goal>testCompile</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <pluginRepositories>
+         <pluginRepository>
+               <id>jcenter</id>
+               <name>JCenter Repository</name>
+               <url>https://jcenter.bintray.com/</url>
+               <releases>
+                   <enabled>true</enabled>
+                   <updatePolicy>never</updatePolicy>
+               </releases>
+               <snapshots>
+                   <enabled>false</enabled>
+               </snapshots>
+         </pluginRepository>
+         <pluginRepository>
+             <id>artifactory</id>
+             <name>JFrog Artifactory Repository</name>
+             <url>https://openhab.jfrog.io/openhab/libs-release</url>
+             <releases>
+                 <enabled>true</enabled>
+                 <updatePolicy>never</updatePolicy>
+             </releases>
+             <snapshots>
+                 <enabled>false</enabled>
+             </snapshots>
+         </pluginRepository>
+       </pluginRepositories>
+
+	   <repositories>
+        
+	        <!-- releases -->
+	        <repository>
+	            <id>jcenter</id>
+	            <name>JCenter Repository</name>
+	            <url>https://jcenter.bintray.com/</url>
+	            <releases>
+	                <enabled>true</enabled>
+	                <updatePolicy>never</updatePolicy>
+	            </releases>
+	            <snapshots>
+	                <enabled>false</enabled>
+	            </snapshots>
+	        </repository>
+
+	        <repository>
+	            <id>openhab-artifactory-release</id>
+	            <name>JFrog Artifactory Repository</name>
+	            <url>https://openhab.jfrog.io/openhab/libs-release</url>
+	            <releases>
+	                <enabled>true</enabled>
+	                <updatePolicy>never</updatePolicy>
+	            </releases>
+	            <snapshots>
+	                <enabled>false</enabled>
+	            </snapshots>
+	        </repository>
+
+	        <!-- snapshots -->
+	        <repository>
+	            <id>openhab-artifactory-snapshot</id>
+	            <name>JFrog Artifactory Repository</name>
+	            <url>https://openhab.jfrog.io/openhab/libs-snapshot</url>
+	            <releases>
+	                <enabled>false</enabled>
+	            </releases>
+	            <snapshots>
+	                <enabled>true</enabled>
+	                <updatePolicy>always</updatePolicy>
+	            </snapshots>
+	        </repository>
+
+	        <!-- SmartHome p2 repository -->
+	        <repository>
+	            <id>p2-smarthome</id>
+	            <url>https://openhab.jfrog.io/openhab/eclipse-smarthome-stable</url>
+	            <layout>p2</layout>
+	        </repository>
+
+	        <!-- openHAB dependencies p2 repository -->
+	        <repository>
+	            <id>p2-openhab-deps-repo</id>
+	            <url>https://dl.bintray.com/openhab/p2/openhab-deps-repo/${ohdr.version}</url>
+	            <layout>p2</layout>
+	        </repository>
+
+	    </repositories>
+
+</project>

          
A => src/main/java/org/openhab/binding/lutronmqtt/LutronMQTTBindingConstants.java +64 -0
@@ 0,0 1,64 @@ 
+/**
+ * Copyright (c) 2014,2018 by the respective copyright holders.
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.lutronmqtt;
+
+import com.google.common.collect.ImmutableSet;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+
+import java.util.Set;
+
+/**
+ * The {@link LutronMQTTBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author William Welliver - Initial contribution
+ */
+ @NonNullByDefault
+public class LutronMQTTBindingConstants {
+
+    private static final String BINDING_ID = "lutronmqtt";
+
+    public final static ThingTypeUID THING_TYPE_MQTTHUB = new ThingTypeUID(BINDING_ID, "hub");
+    public final static ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmableLight");
+    public final static ThingTypeUID THING_TYPE_VARIABLE_FAN = new ThingTypeUID(BINDING_ID, "variableFan");
+    public final static ThingTypeUID THING_TYPE_REMOTE = new ThingTypeUID(BINDING_ID, "remote");
+
+    public final static Set<ThingTypeUID> SUPPORTED_HUBS_UIDS = ImmutableSet.of(THING_TYPE_MQTTHUB);
+
+    public final static Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = ImmutableSet.of(THING_TYPE_DIMMABLE_LIGHT,
+             THING_TYPE_VARIABLE_FAN, THING_TYPE_REMOTE);
+
+    // List of all Channel ids
+    public final static String PROPERTY_UUID = "uuid";
+    public final static String PROPERTY_URL = "url";
+
+    public final static String PROPERTY_OBJECT_ID = "objectId";
+    public final static String PROPERTY_OBJECT_NAME = "name";
+    public final static String PROPERTY_INTEGRATION_ID = "integrationId";
+    public final static String PROPERTY_LINK_ADDRESS = "linkAddress";
+
+    public final static String CONFIG_TOKEN = "token";
+
+    // List of all Channel ids
+    public final static String CHANNEL_LIGHT_LEVEL = "lightlevel";
+    public final static String CHANNEL_LIGHT_STATE = "lightstate";
+
+    public final static String CHANNEL_LIGHT_COLORTEMPERATURE = "colorTemperature";
+    public final static String CHANNEL_LIGHT_COLOR = "color";
+
+    public final static String CHANNEL_FAN_SPEED = "fanSpeed";
+    public final static String CHANNEL_POWER_SWITCH = "powerSwitch";
+
+    public final static int LUTRON_PROPERTY_LEVEL = 1;
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTDeviceDiscoveryService.java +143 -0
@@ 0,0 1,143 @@ 
+package org.openhab.binding.lutronmqtt.discovery;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants;
+import org.openhab.binding.lutronmqtt.handler.DeviceStatusListener;
+import org.openhab.binding.lutronmqtt.handler.LutronMQTTHubHandler;
+import org.openhab.binding.lutronmqtt.model.LutronDevice;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.openhab.binding.lutronmqtt.internal.LutronMQTTHandlerFactory.SUPPORTED_THING_TYPES_UIDS;
+
+public class LutronMQTTDeviceDiscoveryService extends AbstractDiscoveryService implements DeviceStatusListener {
+    private final static int SEARCH_TIME = 60;
+    private final Logger logger = LoggerFactory.getLogger(LutronMQTTDeviceDiscoveryService.class);
+
+    private LutronMQTTHubHandler hubHandler;
+
+    public LutronMQTTDeviceDiscoveryService(LutronMQTTHubHandler hubHandler) {
+        super(SEARCH_TIME);
+        this.hubHandler = hubHandler;
+    }
+
+    public void activate() {
+        logger.warn("activate");
+        hubHandler.registerDeviceStatusListener(this);
+        startScan();
+    }
+
+    @Override
+    public void deactivate() {
+        removeOlderResults(new Date().getTime());
+        hubHandler.unregisterDeviceStatusListener(this);
+    }
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypes() {
+        return SUPPORTED_THING_TYPES_UIDS;
+    }
+
+    @Override
+    public void startScan() {
+        scheduler.schedule(new Runnable() {
+            @Override
+            public void run() {
+                doScan();
+            }
+        }, 5, TimeUnit.SECONDS);
+    }
+
+    protected void doScan() {
+        logger.warn("starting scan");
+        Collection<LutronDevice> devices = hubHandler.getDevices();
+
+        for (LutronDevice d : devices) {
+            onDeviceFound(d);
+        }
+    }
+
+    @Override
+    public void onDeviceFound(LutronDevice d) {
+        ThingTypeUID thingTypeUID = getThingTypeUID(d);
+        if (thingTypeUID == null) {
+            return;
+        }
+
+        ThingUID thingUID = getThingUID(d);
+
+        if (thingUID != null) {
+            if (hubHandler.getThingByUID(thingUID) != null) {
+                logger.debug("ignoring already registered device of name '{}' with id {}", d.getName(), d.getId());
+            }
+            ThingUID bridgeUID = hubHandler.getThing().getUID();
+            Map<String, Object> properties = new HashMap<>(1);
+            properties.put(LutronMQTTBindingConstants.PROPERTY_OBJECT_ID, "" + d.getId());
+            properties.put(LutronMQTTBindingConstants.PROPERTY_INTEGRATION_ID, "" + d.getIntegrationId());
+            properties.put(LutronMQTTBindingConstants.PROPERTY_LINK_ADDRESS, "" + d.getLinkAddress());
+            properties.put(LutronMQTTBindingConstants.PROPERTY_OBJECT_NAME, d.getName());
+
+            logger.warn("discovery result " + d.getName() + " " + d.getIntegrationId());
+
+            DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
+                    .withProperties(properties).withBridge(bridgeUID).withLabel(d.getName()).build();
+
+            thingDiscovered(discoveryResult);
+        } else {
+            logger.debug("discovered unsupported device of name '{}' with id {}", d.getName(), d.getId());
+        }
+
+    }
+
+    @Override
+    protected synchronized void stopScan() {
+        super.stopScan();
+        removeOlderResults(getTimestampOfLastScan());
+    }
+
+    private ThingUID getThingUID(LutronDevice device) {
+        ThingUID bridgeUID = hubHandler.getThing().getUID();
+        ThingTypeUID thingTypeUID = getThingTypeUID(device);
+
+        if (thingTypeUID != null && getSupportedThingTypes().contains(thingTypeUID)) {
+            return new ThingUID(thingTypeUID, bridgeUID, "" + device.getId());
+        } else {
+            return null;
+        }
+    }
+
+    private ThingTypeUID getThingTypeUID(LutronDevice device) {
+        ThingTypeUID thingTypeId = null;
+        String name = device.getName();
+            if (name.endsWith("Remote")) {
+                thingTypeId = LutronMQTTBindingConstants.THING_TYPE_REMOTE;
+            } else if (name.contains("Fan")) {
+                thingTypeId = LutronMQTTBindingConstants.THING_TYPE_VARIABLE_FAN;
+            } else {
+                thingTypeId = LutronMQTTBindingConstants.THING_TYPE_DIMMABLE_LIGHT;
+            }
+        return thingTypeId;
+    }
+
+    @Override
+    public void onDeviceRemoved(LutronDevice d) {
+        // TODO Remove devices
+
+    }
+
+    @Override
+    public void onDeviceStateChanged(LutronDevice light) {
+        // Do nothing
+    }
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTHubDiscoveryParticipant.java +115 -0
@@ 0,0 1,115 @@ 
+package org.openhab.binding.lutronmqtt.discovery;
+
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
+import org.eclipse.smarthome.config.discovery.DiscoveryServiceCallback;
+import org.eclipse.smarthome.config.discovery.ExtendedDiscoveryService;
+import org.eclipse.smarthome.config.discovery.mdns.MDNSDiscoveryParticipant;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jmdns.ServiceInfo;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.*;
+
+@Component(service = MDNSDiscoveryParticipant.class, immediate = true, configurationPid = "binding.lutronmqtt", name = "org.openhab.binding.lutronmqtt.discovery.hub")
+public class LutronMQTTHubDiscoveryParticipant implements MDNSDiscoveryParticipant, ExtendedDiscoveryService {
+    private Logger logger = LoggerFactory.getLogger(LutronMQTTHubDiscoveryParticipant.class);
+    private DiscoveryServiceCallback discoveryServiceCallback;
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
+        return Collections.singleton(LutronMQTTBindingConstants.THING_TYPE_MQTTHUB);
+    }
+
+    @Override
+    public String getServiceType() {
+        return "_lutron_mqtt._tcp.local.";
+    }
+
+    @Override
+    public DiscoveryResult createResult(ServiceInfo service) {
+logger.warn("Discovery result: " + service.toString());
+            ThingUID uid = getThingUID(service);
+
+            logger.warn("text: " + service.getTextString());
+            logger.warn("server: " + service.getServer());
+            logger.warn("name: " + service.getName());
+            logger.warn("qname: " + service.getQualifiedName());
+            logger.warn("nice: " + service.getNiceTextString());
+            Enumeration<String> pn = service.getPropertyNames();
+            while(pn.hasMoreElements())
+                logger.warn("prop: " + pn.nextElement());
+
+            logger.debug("Got discovered device.");
+            if (true /*getDiscoveryServiceCallback() != null*/) {
+              /*  logger.debug("Looking to see if this thing exists already.");
+                Thing thing = getDiscoveryServiceCallback().getExistingThing(uid);
+                if (thing != null) {
+                    logger.debug("Already have thing with ID=<" + uid + ">");
+                    return null;
+                } else {
+                    logger.debug("Nope. This should trigger a new inbox entry.");
+                }
+            } else {
+                logger.warn("DiscoveryServiceCallback not set. This shouldn't happen!");
+                return null;
+            }
+
+*/
+                if (uid != null) {
+                    Map<String, Object> properties = new HashMap<>(2);
+                    String u = service.getPropertyString("uuid");
+                    String s = service.getServer();
+
+                    if(u == null || s == null || u.isEmpty() || s.isEmpty()) // incomplete discovery result
+                    {
+                        logger.warn("Found lutron-mqtt service but missing data. Try restarting device.");
+                        return null;
+                    }
+
+                        properties.put(LutronMQTTBindingConstants.PROPERTY_URL, "tcp://" + s +
+                                ":" + service.getPort());
+                    properties.put(LutronMQTTBindingConstants.PROPERTY_UUID, u);
+
+                    return DiscoveryResultBuilder.create(uid).withProperties(properties)
+                            .withRepresentationProperty(uid.getId()).withLabel(service.getName() + " Lutron-MQTT Gateway").build();
+                }
+            }
+        return null;
+
+    }
+
+    @Override
+    public ThingUID getThingUID(ServiceInfo service) {
+
+        if (service != null) {
+            if (service.getType() != null) {
+                if (service.getType().equals(getServiceType())) {
+                    logger.trace("Discovered a Lutron-MQTT gateway thing with name '{}'", service.getName());
+                    return new ThingUID(LutronMQTTBindingConstants.THING_TYPE_MQTTHUB, service.getName().replace(" ", "_"));
+                }
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public void setDiscoveryServiceCallback(DiscoveryServiceCallback discoveryServiceCallback) {
+        logger.warn(discoveryServiceCallback.toString());
+        this.discoveryServiceCallback = discoveryServiceCallback;
+        // log.debug(discoveryServiceCallback.toString());
+    }
+
+    public DiscoveryServiceCallback getDiscoveryServiceCallback() {
+        return discoveryServiceCallback;
+    }
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/handler/DeviceStatusListener.java +13 -0
@@ 0,0 1,13 @@ 
+package org.openhab.binding.lutronmqtt.handler;
+
+import org.openhab.binding.lutronmqtt.model.LutronDevice;
+
+public interface DeviceStatusListener {
+
+    public void onDeviceFound(LutronDevice d);
+
+    public void onDeviceRemoved(LutronDevice d);
+
+    public void onDeviceStateChanged(LutronDevice light);
+
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LightStateConverter.java +156 -0
@@ 0,0 1,156 @@ 
+package org.openhab.binding.lutronmqtt.handler;
+
+import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.openhab.binding.lutronmqtt.model.LutronDevice;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.LUTRON_PROPERTY_LEVEL;
+
+/**
+ * The {@link LightStateConverter} is responsible for mapping Eclipse SmartHome
+ * types to jue types and vice versa.
+ *
+ * @author Dennis Nobel - Initial contribution
+ * @author Oliver Libutzki - Adjustments
+ * @author Kai Kreuzer - made code static
+ * @author Andre Fuechsel - added method for brightness
+ * @author Yordan Zhelev - added method for alert
+ * @author Denis Dudnik - switched to internally integrated source of Jue library, minor code cleanup
+ *
+ */
+public class LightStateConverter {
+
+    private static final double BRIGHTNESS_FACTOR = 1.0;
+
+    private static final int DIM_STEPSIZE = 30;
+
+    protected static Logger log = LoggerFactory.getLogger(LightStateConverter.class);
+
+
+    /**
+     * Transforms the given {@link OnOffType} into a light state containing the
+     * 'on' value.
+     *
+     * @param onOffType
+     *            on or off state
+     * @return light state containing the 'on' value
+     */
+    public static int toOnOffLightState(OnOffType onOffType) {
+        int f = 0;
+        if(onOffType == OnOffType.ON) f = 65535;
+        return f;
+    }
+
+    public static int toPercentLightState(PercentType percentType) {
+        int f = 0;
+        if(PercentType.ZERO.equals(percentType))
+            f = 0;
+        else if(PercentType.HUNDRED.equals(percentType))
+            f = 65535;
+        else {
+            f = (int) Math.round(percentType.floatValue() * 65535 / 100);
+        }
+        return f;
+    }
+
+    public static Map<String,Object> toLightState(OnOffType onOffType, LutronDevice device) {
+        int level = toOnOffLightState(onOffType);
+        Map<String,Object> m = makeGoToLevelCommand(level, device);
+        return m;
+    }
+
+    public static Map<String,Object> makeGoToLevelCommand(int level, LutronDevice device) {
+        Map<String,Object> a = new HashMap<>();
+        log.warn("device: " + device);
+        a.put("ObjectId", device.getLinkAddress());
+        a.put("ObjectType", 15);
+        a.put("Fade", 7);
+        a.put("Delay", 0);
+        a.put("Level", level);
+        Map<String,Object> m = new HashMap<>();
+        m.put("cmd", "GoToLevel");
+        m.put("args", a);
+
+        return m;
+    }
+
+    /**
+     * Transforms the given {@link PercentType} into a light state containing
+     * the brightness and the 'on' value represented by {@link PercentType}.
+     *
+     * @param percentType
+     *            brightness represented as {@link PercentType}
+     * @return light state containing the brightness and the 'on' value
+     */
+    public static Map<String,Object> toLightState(PercentType percentType, LutronDevice device) {
+        int level = toPercentLightState(percentType);
+
+        Map<String,Object> m = makeGoToLevelCommand(level, device);
+
+        return m;
+    }
+
+    public static Map<String,Object> toLightState(IncreaseDecreaseType increaseDecreaseType, LutronDevice device) {
+        int level = toAdjustedBrightness(increaseDecreaseType, device.getProperty(LUTRON_PROPERTY_LEVEL));
+
+        Map<String,Object> m = makeGoToLevelCommand(level, device);
+
+        return m;
+    }
+
+    /**
+     * Adjusts the given brightness using the {@link IncreaseDecreaseType} and
+     * returns the updated value.
+     *
+     * @param command
+     *            The {@link IncreaseDecreaseType} to be used
+     * @param currentBrightness
+     *            The current brightness
+     * @return The adjusted brightness value
+     */
+    public static int toAdjustedBrightness(IncreaseDecreaseType command, int currentBrightness) {
+        int newBrightness;
+        if (command == IncreaseDecreaseType.DECREASE) {
+            newBrightness = Math.max(currentBrightness - DIM_STEPSIZE, 0);
+        } else {
+            newBrightness = Math.min(currentBrightness + DIM_STEPSIZE, 65535);
+        }
+        return newBrightness;
+    }
+
+
+    /**
+     * Transforms Luton device state into {@link PercentType} representing
+     * the brightness.
+     *
+     * @param device
+     *            lutron device
+     * @return percent type representing the brightness
+     */
+    public static PercentType toBrightnessPercentType(LutronDevice device) {
+        int percent = (int) Math.round(device.getProperty(LUTRON_PROPERTY_LEVEL) / (65535/100));
+        if (log.isTraceEnabled()) {
+            log.trace("Converting " + device.getProperty(LUTRON_PROPERTY_LEVEL) + " -> " + percent + " -> "
+                    + new PercentType(restrictToBounds(percent)));
+        }
+        return new PercentType(restrictToBounds(percent));
+    }
+
+
+    private static int restrictToBounds(int percentValue) {
+        if (percentValue < 0) {
+            return 0;
+        } else if (percentValue > 100) {
+            return 100;
+        }
+        return percentValue;
+    }
+
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTDimmableLightHandler.java +27 -0
@@ 0,0 1,27 @@ 
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.openhab.binding.lutronmqtt.handler;
+
+import org.eclipse.smarthome.core.thing.Thing;
+
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.CHANNEL_LIGHT_LEVEL;
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.CHANNEL_LIGHT_STATE;
+
+/**
+ * The {@link LutronMQTTDimmableLightHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author William Welliver - Initial contribution
+ */
+public class LutronMQTTDimmableLightHandler extends PowerLevelDeviceHandler {
+
+    public LutronMQTTDimmableLightHandler(Thing thing) {
+        super(thing, CHANNEL_LIGHT_LEVEL, CHANNEL_LIGHT_STATE);
+    }
+
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTHubHandler.java +364 -0
@@ 0,0 1,364 @@ 
+/**
+ * Copyright (c) 2014,2018 by the respective copyright holders.
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.lutronmqtt.handler;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gson.Gson;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.paho.client.mqttv3.*;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+import org.eclipse.smarthome.config.core.Configuration;
+import org.eclipse.smarthome.core.thing.*;
+import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
+import org.eclipse.smarthome.core.types.Command;
+import org.openhab.binding.lutronmqtt.internal.LutronMQTTConfiguration;
+import org.openhab.binding.lutronmqtt.model.LutronDevice;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.util.*;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.CONFIG_TOKEN;
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.PROPERTY_URL;
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.PROPERTY_UUID;
+
+/**
+ * The {@link LutronMQTTHubHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author William Welliver - Initial contribution
+ */
+public class LutronMQTTHubHandler extends BaseBridgeHandler implements MqttCallback {
+
+    private final Logger logger = LoggerFactory.getLogger(LutronMQTTHubHandler.class);
+
+    private Collection<DeviceStatusListener> deviceStatusListeners = new HashSet<>();
+
+    private List<LutronDevice> deviceList = Collections.EMPTY_LIST;
+
+    @Nullable
+    private LutronMQTTConfiguration config;
+    private String token;
+    private MqttClient mqttClient;
+    private Gson gson = new Gson();
+    private Map<Integer, LutronDevice> devicesByLinkAddress = new HashMap<>();
+    private ScheduledFuture<?> reconnectJob;
+    private ScheduledFuture<?> onlineTimeout;
+    private ScheduledFuture<?> allItemsJob;
+
+    public LutronMQTTHubHandler(Bridge thing) {
+        super(thing);
+    }
+
+    @Override
+    public void initialize() {
+        updateStatus(ThingStatus.UNKNOWN);
+        Configuration config = getThing().getConfiguration();
+        Map<String, String> properties = getThing().getProperties();
+        token = (String) config.get(CONFIG_TOKEN);
+
+        final String uuid = properties.get(PROPERTY_UUID);
+        final String broker = properties.get(PROPERTY_URL);
+
+        updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
+
+        MemoryPersistence persistence = new MemoryPersistence();
+        String clientId = "openhab-lutron-mqtt-" + (System.currentTimeMillis()/1000);
+
+        try {
+            if(mqttClient == null || !mqttClient.isConnected()) {
+                logger.info("Attempting to connect to MQTT Broker.");
+                MqttClient client = new MqttClient(broker, clientId, persistence);
+                MqttConnectOptions connOpts = new MqttConnectOptions();
+//                connOpts.setCleanSession(true);
+//                connOpts.setKeepAliveInterval(30);
+//                connOpts.setConnectionTimeout(90);
+                client.connect(connOpts);
+                mqttClient = client;
+                setupSubscriptions();
+            }
+        } catch (MqttException e) {
+            logger.error("Unable to connect to MQTT broker at " + broker, e);
+            goOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Unable to connect to MQTT broker at broker");
+        }
+    }
+
+
+
+    @Override
+    public void dispose() {
+        logger.info("Disposing of handler " + this);
+        super.dispose();
+        try {
+            MqttClient cl = mqttClient;
+            cl.setCallback(null);
+            cl.unsubscribe(new String[] {"lutron/status", "lutron/events", "lutron/remote"});
+            cl.disconnectForcibly(3000);
+            cl.close();
+            mqttClient = null;
+            cancelJobs();
+
+        } catch (MqttException e) {
+            logger.warn("Error while disconnecting", e);
+        }
+    }
+
+    private void cancelJobs() {
+        if(reconnectJob != null && !reconnectJob.isCancelled())
+            reconnectJob.cancel(true);
+        if(allItemsJob != null && !allItemsJob.isCancelled())
+            allItemsJob.cancel(true);
+        if(onlineTimeout != null && !allItemsJob.isCancelled())
+            onlineTimeout.cancel(true);
+    }
+
+    private void setupSubscriptions() {
+        mqttClient.setCallback(this);
+        try {
+            mqttClient.subscribe(new String[] {"lutron/status", "lutron/events", "lutron/remote"});
+        } catch (MqttException e) {
+            logger.error("subscribe failed.", e);
+            goOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Unable to subscribe to events");
+        }
+
+        cancelJobs();
+
+        allItemsJob = scheduler.schedule(new Runnable() {
+            @Override
+            public void run() {
+                requestAllItems();
+            }
+        }, 5, TimeUnit.SECONDS);
+    }
+
+    private void requestAllItems() {
+        try {
+            byte[] b = ("{\"cmd\": \"GetDevices\", \"args\": {}}").getBytes("UTF-8");
+            mqttClient.publish("lutron/commands", (b), 0, false);
+        } catch (MqttException e) {
+            logger.warn("Error Sending message.", e);
+        } catch (UnsupportedEncodingException e) {
+            logger.warn("Error encoding message.", e);
+        }
+
+        allItemsJob = scheduler.schedule(new Runnable() {
+            @Override
+            public void run() {
+                requestAllItems();
+            }
+        }, 5, TimeUnit.MINUTES);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+
+    }
+
+    protected void goOnline() {
+        updateStatus(ThingStatus.ONLINE);
+    }
+
+    protected void goOffline(ThingStatusDetail detail, String reason) {
+        updateStatus(ThingStatus.OFFLINE, detail, reason);
+        if(onlineTimeout != null) onlineTimeout.cancel(true);
+        if(allItemsJob != null) onlineTimeout.cancel(true);
+    }
+
+    public Collection<LutronDevice> getDevices() {
+        return ImmutableList.copyOf(deviceList);
+    }
+
+    public void registerDeviceStatusListener(DeviceStatusListener lutronDeviceDiscoveryService) {
+        deviceStatusListeners.add(lutronDeviceDiscoveryService);
+    }
+
+    public void unregisterDeviceStatusListener(DeviceStatusListener lutronDeviceDiscoveryService) {
+        deviceStatusListeners.remove(lutronDeviceDiscoveryService);
+    }
+
+    @Override
+    public void connectionLost(Throwable throwable) {
+        goOffline(ThingStatusDetail.BRIDGE_OFFLINE, throwable.getMessage());
+        logger.warn("Lost connection to MQTT server. Will rety in 20 seconds.", throwable);
+        if(mqttClient != null) scheduleReconnect(20);
+    }
+
+    private void scheduleReconnect(int seconds) {
+       if(reconnectJob == null || reconnectJob.isDone()) {
+           reconnectJob = scheduler.schedule(new Runnable() {
+               @Override
+               public void run() {
+                   reconnect();
+               }
+           }, seconds, TimeUnit.SECONDS);
+       }
+    }
+
+    private void reconnect() {
+        if(mqttClient == null) {
+            logger.info("Skipping reconnect because MQTT client is null.");
+            return;
+        }
+        logger.info("Attempting to reconnect to MQTT server");
+            initialize();
+        ThingStatusInfo info = getThing().getStatusInfo();
+        if(info.getStatus() != ThingStatus.ONLINE &&
+                (info.getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE ||
+                        info.getStatusDetail() == ThingStatusDetail.COMMUNICATION_ERROR
+                )) // we're not online but have a retryable status
+            scheduleReconnect(20);
+        else {
+            reconnectJob = null;
+        }
+    }
+
+    @Override
+    public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
+        logger.debug("messageArrived: " + s + " " + mqttMessage.toString());
+        switch(s) {
+            case "lutron/status":
+                handleStatusMessage(mqttMessage);
+                break;
+            case "lutron/remote":
+                handleRemoteEvent(mqttMessage);
+                break;
+            case "lutron/events":
+                handleGatewayEvent(mqttMessage);
+                break;
+        }
+    }
+
+    private void handleGatewayEvent(MqttMessage mqttMessage) {
+        //logger.info("gateway event: " + new String(mqttMessage.getPayload()));
+        Map <String,Object> msg = gson.fromJson(new String(mqttMessage.getPayload()), HashMap.class);
+
+        if(msg.containsKey("cmd")) {
+            String key = (String)msg.get("cmd");
+            Object arg = msg.get("args");
+            switch(key) {
+                case "ListDevices":
+                    int i = 0;
+                    logger.warn("ListDevices Response: " + arg);
+                    List<LutronDevice> devices = new ArrayList<>();
+                    for(Map<String,Object> objectMap : (List<Map<String,Object>>)arg) {
+                        getThing().getThings();
+                        final LutronDevice device = new LutronDevice(objectMap);
+                        logger.info("Device: " + device);
+                        devices.add(device);
+                        if(devicesByLinkAddress.containsKey(device.getLinkAddress())) {
+                            // TODO copy properties to new device
+                        }
+                        devicesByLinkAddress.put(device.getLinkAddress(), device);
+                        scheduler.schedule(new Runnable() {
+                            @Override
+                            public void run() {
+                                requestUpdateForDevice(device.getLinkAddress());
+                            }
+                        }, i++%3, TimeUnit.SECONDS);
+                    }
+
+                    this.deviceList = devices;
+
+                    break;
+                case "RuntimePropertyUpdate":
+                    int linkAddress = ((Number)(((Map)arg).get("ObjectId"))).intValue();
+                    LutronDevice device = getDeviceByLinkAddress(linkAddress);
+                    if(device == null) {
+                        logger.warn("Unable to find LutronDevice with linkAddress="  + linkAddress);
+                    }
+                    List<List<Object>> l = (List<List<Object>>)(((Map)arg).get("Properties"));
+                    for(List property : l) {
+                        int pnum = ((Number)(property.get(0))).intValue();
+                        int pval = ((Number)(property.get(1))).intValue();
+                        device.putProperty(pnum, pval);
+                    }
+                    informDeviceListeners(device);
+
+
+                    break;
+                default:
+                    logger.warn("Received unknown message type " + key + " with args " + arg);
+                    break;
+            }
+        }
+    }
+
+    protected void requestUpdateForDevice(int linkAddress) {
+        try {
+            byte[] b = ("{\"cmd\":\"RuntimePropertyQuery\", \"args\":{\"Params\":[[" + linkAddress + ",15,[1]]]}}").getBytes();
+            MqttMessage message = new MqttMessage(b);
+            mqttClient.publish("lutron/commands", message);
+        } catch (MqttException e) {
+            logger.error("Error publishing message", e);
+        }
+    }
+
+    protected void informDeviceListeners(LutronDevice device) {
+        logger.info("informDeviceListeners " + device);
+        for (DeviceStatusListener s : deviceStatusListeners) {
+            s.onDeviceStateChanged(device);
+        }
+    }
+
+    public LutronDevice getDeviceByLinkAddress(int linkAddress) {
+        logger.warn("looking for device with link address =" + linkAddress);
+        return devicesByLinkAddress.get(linkAddress);
+    }
+
+    private void handleRemoteEvent(MqttMessage mqttMessage) {
+        logger.info("remote event: " + new String(mqttMessage.getPayload()));
+
+        Map <String,Object> msg = gson.fromJson(new String(mqttMessage.getPayload()), HashMap.class);
+       // {"serial" : "C726CA", "action": "down", "button": "select"}
+
+    }
+
+    private void handleStatusMessage(MqttMessage mqttMessage) {
+        logger.info("status message: " + new String(mqttMessage.getPayload()));
+        Map <String,Object> msg = gson.fromJson(new String(mqttMessage.getPayload()), HashMap.class);
+        if(msg.containsKey("state") && "running".equals(msg.get("state"))) {
+            if(onlineTimeout != null)
+                onlineTimeout.cancel(true);
+            goOnline();
+            onlineTimeout = scheduler.schedule(new Runnable() {
+                @Override
+                public void run() {
+                    onlineTimeoutOccurred();
+                }
+            },120, TimeUnit.SECONDS);
+        }
+    }
+
+    protected void onlineTimeoutOccurred() {
+        goOffline(ThingStatusDetail.BRIDGE_OFFLINE, "Too long between status announcements.");
+    }
+
+    @Override
+    public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
+        // We really don't care too much about this status.
+    }
+
+    public void setDesiredState(int deviceId, Map<String, Object> lightState) {
+        try {
+            byte[] bytes = (gson.toJson(lightState)).getBytes("UTF-8");
+           // MqttMessage message = new MqttMessage(bytes);
+            mqttClient.publish("lutron/commands", bytes, 0, false);
+            logger.info("Sent message.");
+        } catch (MqttException|UnsupportedEncodingException e) {
+            e.printStackTrace();
+        }
+    }
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTRemoteHandler.java +50 -0
@@ 0,0 1,50 @@ 
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.openhab.binding.lutronmqtt.handler;
+
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.types.Command;
+import org.openhab.binding.lutronmqtt.model.LutronDevice;
+
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.CHANNEL_LIGHT_LEVEL;
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.CHANNEL_LIGHT_STATE;
+
+/**
+ * The {@link LutronMQTTRemoteHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author William Welliver - Initial contribution
+ */
+public class LutronMQTTRemoteHandler extends BaseThingHandler implements DeviceStatusListener {
+
+    public LutronMQTTRemoteHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+
+    }
+
+    @Override
+    public void onDeviceFound(LutronDevice d) {
+
+    }
+
+    @Override
+    public void onDeviceRemoved(LutronDevice d) {
+
+    }
+
+    @Override
+    public void onDeviceStateChanged(LutronDevice light) {
+
+    }
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTVariableFanHandler.java +26 -0
@@ 0,0 1,26 @@ 
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.openhab.binding.lutronmqtt.handler;
+
+import org.eclipse.smarthome.core.thing.Thing;
+
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.CHANNEL_FAN_SPEED;
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.CHANNEL_POWER_SWITCH;
+
+/**
+ * The {@link LutronMQTTVariableFanHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author William Welliver - Initial contribution
+ */
+public class LutronMQTTVariableFanHandler extends PowerLevelDeviceHandler {
+
+    public LutronMQTTVariableFanHandler(Thing thing) {
+        super(thing, CHANNEL_FAN_SPEED, CHANNEL_POWER_SWITCH);
+    }
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/handler/PowerLevelDeviceHandler.java +264 -0
@@ 0,0 1,264 @@ 
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.openhab.binding.lutronmqtt.handler;
+
+import com.google.gson.Gson;
+import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.core.thing.Bridge;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.ThingStatusInfo;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.types.Command;
+import org.openhab.binding.lutronmqtt.model.LutronDevice;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.*;
+
+/**
+ * The {@link PowerLevelDeviceHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author William Welliver - Initial contribution
+ */
+public class PowerLevelDeviceHandler extends BaseThingHandler implements DeviceStatusListener {
+
+    protected Logger log = LoggerFactory.getLogger(getClass());
+
+    protected int deviceId; //
+    protected int integrationId;
+    protected LutronDevice device; // last update received for this device.
+    protected int linkAddress;
+
+    protected LutronMQTTHubHandler hubHandler;
+
+    protected final String powerLevelChannelName;
+    // protected final String powerSwitchChannelName;
+
+    public PowerLevelDeviceHandler(Thing thing, String powerLevelChannel, String powerSwitchChannel) {
+        super(thing);
+        this.powerLevelChannelName = powerLevelChannel;
+        // this.powerSwitchChannelName = powerSwitchChannel;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        Map<String,Object> lightState = null;
+        String ch = channelUID.getId();
+        log.warn("Got a command for channel id=" + ch  + ", command=" + command);
+
+        if (log.isTraceEnabled()) {
+            log.trace("command= " + command + ", last device reading=" + getDevice().getProperty(LUTRON_PROPERTY_LEVEL));
+        }
+        if (powerLevelChannelName.equals(ch)) {
+            if (command instanceof PercentType) {
+                lightState = LightStateConverter.toLightState((PercentType) command, getDevice());
+            } else if (command instanceof OnOffType) {
+                lightState = LightStateConverter.toLightState((OnOffType) command, getDevice());
+            } else if (command instanceof IncreaseDecreaseType) {
+                lightState = LightStateConverter.toLightState((IncreaseDecreaseType) command, getDevice());
+            }
+        }
+
+        /*
+         * else if (powerSwitchChannelName.equals(ch)) {
+         * if (command instanceof OnOffType) {
+         * lightState = LightStateConverter.toOnOffLightState((OnOffType) command, getDevice());
+         * }
+         * }
+         */
+
+        if (lightState != null) {
+            if (log.isTraceEnabled()) {
+                Gson gson = new Gson();
+                log.trace("converted " + command + " to " + gson.toJson(lightState));
+            }
+            updateDeviceState(lightState);
+        } else {
+            log.warn("Got a command for an unhandled channel: " + ch);
+        }
+    }
+
+    protected void updateDeviceState(Map<String, Object> lightState) {
+        log.warn("updateDeviceState: " + lightState);
+        getHubHandler().setDesiredState(deviceId, lightState);
+    }
+
+    @Override
+    public void initialize() {
+        log.debug("Initializing power level device handler.");
+        initializeThing((getBridge() == null) ? null : getBridge().getStatus());
+    }
+
+    private void initializeThing(ThingStatus bridgeStatus) {
+        log.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
+        final String configDeviceId = getThing().getProperties().get(PROPERTY_OBJECT_ID);
+        log.warn("intializeThing " + getThing().getProperties().get(PROPERTY_INTEGRATION_ID));
+        integrationId = Integer.parseInt(getThing().getProperties().get(PROPERTY_INTEGRATION_ID));
+        linkAddress = Integer.parseInt(getThing().getProperties().get(PROPERTY_LINK_ADDRESS));
+
+        if (configDeviceId != null) {
+            deviceId = Integer.valueOf(configDeviceId);
+
+            if (getHubHandler() != null) {
+                if (bridgeStatus == ThingStatus.ONLINE) {
+                    getHubHandler().requestUpdateForDevice(linkAddress);
+                    LutronDevice device = getHubHandler().getDeviceByLinkAddress(linkAddress);
+                    if (device == null) {
+                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+                        return;
+                    }
+                    updateStatus(ThingStatus.ONLINE);
+
+                    // receiving a response to the request update method above should trigger a state change.
+                    //onDeviceStateChanged(getHubHandler().getDeviceByIntegrationId(integrationId));
+                    // initializeProperties();
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+                }
+            } else {
+                updateStatus(ThingStatus.OFFLINE);
+            }
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
+        }
+    }
+
+    @Override
+    public void dispose() {
+        log.debug("Handler disposed. Unregistering listener.");
+        if (deviceId != 0) { // technically 0 is a valid device id but it appears to be reserved for the hub
+            LutronMQTTHubHandler hubHandler = getHubHandler();
+            if (hubHandler != null) {
+                hubHandler.unregisterDeviceStatusListener(this);
+                this.hubHandler = null;
+            }
+            deviceId = 0;
+        }
+    }
+
+    protected synchronized LutronMQTTHubHandler getHubHandler() {
+        if (this.hubHandler == null) {
+            Bridge bridge = getBridge();
+            if (bridge == null) {
+                return null;
+            }
+            ThingHandler handler = bridge.getHandler();
+            if (handler instanceof LutronMQTTHubHandler) {
+                this.hubHandler = (LutronMQTTHubHandler) handler;
+                this.hubHandler.registerDeviceStatusListener(this);
+            } else {
+                return null;
+            }
+        }
+        return this.hubHandler;
+    }
+
+    protected void scheduleUpdateForDevice(int deviceId) {
+        if (true == true) {
+            return;
+        }
+        log.info("Scheduling an update request for deviceId=" + deviceId);
+        scheduler.submit(new Runnable() {
+            @Override
+            public void run() {
+                LutronMQTTHubHandler handler = getHubHandler();
+                if (handler != null) {
+                    onDeviceStateChanged(handler.getDeviceByLinkAddress(linkAddress));
+                }
+            }
+        });
+    }
+
+    public LutronDevice getDevice() {
+        if (device != null) {
+            return device;
+        }
+
+        LutronMQTTHubHandler handler = getHubHandler();
+        device = handler.getDeviceByLinkAddress(linkAddress);
+        return device;
+    }
+
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatus) {
+        super.bridgeStatusChanged(bridgeStatus);
+        if (bridgeStatus.getStatus() == ThingStatus.ONLINE) {
+            scheduleUpdateForDevice(linkAddress);
+        }
+    }
+
+    @Override
+    public void channelLinked(ChannelUID channelUID) {
+        if (this.getBridge().getStatus() == ThingStatus.ONLINE) {
+            // TODO really only need 1 for each device, no matter the channelse.
+            scheduleUpdateForDevice(linkAddress);
+        } else {
+            log.info("Channel Linked but hub is not online.");
+        }
+    }
+
+    @Override
+    public void onDeviceFound(LutronDevice d) {
+        if (d.getId() == deviceId) {
+            updateStatus(ThingStatus.ONLINE);
+            onDeviceStateChanged(d);
+        }
+    }
+
+    @Override
+    public void onDeviceRemoved(LutronDevice d) {
+        if (d.getId() == deviceId) {
+            updateStatus(ThingStatus.OFFLINE);
+        }
+    }
+
+    @Override
+    public void onDeviceStateChanged(LutronDevice d) {
+        if (d.getLinkAddress() != linkAddress) {
+            return;
+        }
+
+        log.info("Go device status change for " + this.getThing().getLabel());
+
+        if (d.hasUpdatedProperties()) {
+            log.info("Received notice of pending state change.");
+        }
+
+        if (false && device != null && d.getProperty(LUTRON_PROPERTY_LEVEL) == (device.getProperty(LUTRON_PROPERTY_LEVEL))) {
+            log.info("Lutron Device: " + d.getName() + " Received State Changed but no difference");
+            return;
+        }
+
+        device = d;
+
+        log.info("Lutron Device: " + d.getName() + " State Changed: " + d.getProperty(LUTRON_PROPERTY_LEVEL));
+
+        // TODO we should keep the previous state so that we don't send unnecessary updates.
+
+        PercentType percentType = LightStateConverter.toBrightnessPercentType(d);
+
+        log.info("Lutron: " + d.getName() + " Light Level: " + percentType.intValue());
+        updateState(powerLevelChannelName, percentType);
+    }
+
+    protected Integer getCurrentLevel(LutronDevice light) {
+        int brightness = light.getProperty(LUTRON_PROPERTY_LEVEL);
+
+        return brightness;
+    }
+
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTConfiguration.java +26 -0
@@ 0,0 1,26 @@ 
+/**
+ * Copyright (c) 2014,2018 by the respective copyright holders.
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.lutronmqtt.internal;
+
+/**
+ * The {@link LutronMQTTConfiguration} class contains fields mapping thing configuration paramters.
+ *
+ * @author William Welliver - Initial contribution
+ */
+public class LutronMQTTConfiguration {
+
+    /**
+     * Sample configuration parameter. Replace with you own.
+     */
+    public String config1;
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTHandlerFactory.java +101 -0
@@ 0,0 1,101 @@ 
+/**
+ * Copyright (c) 2014,2018 by the respective copyright holders.
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * 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.lutronmqtt.internal;
+
+import static org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants.*;
+
+import java.util.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.config.discovery.DiscoveryService;
+import org.eclipse.smarthome.core.thing.Bridge;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.openhab.binding.lutronmqtt.LutronMQTTBindingConstants;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
+import org.openhab.binding.lutronmqtt.discovery.LutronMQTTDeviceDiscoveryService;
+import org.openhab.binding.lutronmqtt.handler.LutronMQTTDimmableLightHandler;
+import org.openhab.binding.lutronmqtt.handler.LutronMQTTHubHandler;
+import org.openhab.binding.lutronmqtt.handler.LutronMQTTRemoteHandler;
+import org.openhab.binding.lutronmqtt.handler.LutronMQTTVariableFanHandler;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link LutronMQTTHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author William Welliver - Initial contribution
+ */
+@Component(service = ThingHandlerFactory.class, immediate = true, configurationPid = "binding.lutronmqtt", name="lutronmqtt")
+@NonNullByDefault
+public class LutronMQTTHandlerFactory extends BaseThingHandlerFactory {
+
+    public final static Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = LutronMQTTBindingConstants.SUPPORTED_THING_TYPES_UIDS;
+    private Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return THING_TYPE_MQTTHUB.equals(thingTypeUID) || SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected ThingHandler createHandler(Thing thing) {
+
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (thingTypeUID.equals(THING_TYPE_MQTTHUB)) {
+            LutronMQTTHubHandler handler = new LutronMQTTHubHandler((Bridge) thing);
+            registerDeviceDiscoveryService(handler);
+            return handler;
+        } else if (thingTypeUID.equals(THING_TYPE_REMOTE)) {
+            LutronMQTTRemoteHandler handler = new LutronMQTTRemoteHandler(thing);
+            return handler;
+        }
+        else if (thingTypeUID.equals(THING_TYPE_DIMMABLE_LIGHT)) {
+            LutronMQTTDimmableLightHandler handler = new LutronMQTTDimmableLightHandler(thing);
+            return handler;
+        } else if (thingTypeUID.equals(THING_TYPE_VARIABLE_FAN)) {
+            LutronMQTTVariableFanHandler handler = new LutronMQTTVariableFanHandler(thing);
+            return handler;
+        }
+
+        return null;
+    }
+
+    private synchronized void registerDeviceDiscoveryService(LutronMQTTHubHandler hubHandler) {
+        LutronMQTTDeviceDiscoveryService discoveryService = new LutronMQTTDeviceDiscoveryService(hubHandler);
+        discoveryService.activate();
+        this.discoveryServiceRegs.put(hubHandler.getThing().getUID(), bundleContext
+                .registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<String, Object>()));
+    }
+
+    @Override
+    protected synchronized void removeHandler(ThingHandler thingHandler) {
+        if (thingHandler instanceof LutronMQTTHubHandler) {
+            ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.get(thingHandler.getThing().getUID());
+            if (serviceReg != null) {
+                // remove discovery service, if bridge handler is removed
+                LutronMQTTDeviceDiscoveryService service = (LutronMQTTDeviceDiscoveryService) bundleContext
+                        .getService(serviceReg.getReference());
+                service.deactivate();
+                serviceReg.unregister();
+                discoveryServiceRegs.remove(thingHandler.getThing().getUID());
+            }
+        }
+    }
+
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/model/LutronDevice.java +132 -0
@@ 0,0 1,132 @@ 
+package org.openhab.binding.lutronmqtt.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class LutronDevice {
+    int id;
+    String name;
+    int integrationId;
+    String description;
+    int linkAddress;
+    int serialNumber;
+    int deviceClass;
+
+    Map<Integer,Integer> properties = new HashMap<>();
+    private long lastUpdated;
+
+    public LutronDevice(Map<String,Object> d) {
+        setSerialNumber(Integer.parseInt((String)d.get("SerialNumber")));
+        setDeviceClass(Integer.parseInt((String)d.get("DeviceClass")));
+        setDescription((String)d.get("Description"));
+        setLinkAddress(Integer.parseInt((String)d.get("LinkAddress")));
+        setName((String)d.get("Name"));
+        setId(Integer.parseInt((String)d.get("DeviceID")));
+        setIntegrationId(Integer.parseInt((String)d.get("IntegrationID")));
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public int getLinkAddress() { return linkAddress; }
+
+    public void setLinkAddress(int linkAddress) { this.linkAddress = linkAddress; }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getIntegrationId() {
+        return integrationId;
+    }
+
+    public void setIntegrationId(int integrationId) {
+        this.integrationId = integrationId;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public int getSerialNumber() {
+        return serialNumber;
+    }
+
+    public void setSerialNumber(int serialNumber) {
+        this.serialNumber = serialNumber;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public int getDeviceClass() {
+        return deviceClass;
+    }
+
+    public void setDeviceClass(int deviceClass) {
+        this.deviceClass = deviceClass;
+    }
+
+    public int getProperty(int property) {
+        return properties.get(property);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        LutronDevice that = (LutronDevice) o;
+
+        if (id != that.id) return false;
+        if (integrationId != that.integrationId) return false;
+        if (serialNumber != that.serialNumber) return false;
+        if (deviceClass != that.deviceClass) return false;
+        if (name != null ? !name.equals(that.name) : that.name != null) return false;
+        return description != null ? description.equals(that.description) : that.description == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = id;
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        result = 31 * result + integrationId;
+        result = 31 * result + linkAddress;
+        result = 31 * result + (description != null ? description.hashCode() : 0);
+        result = 31 * result + serialNumber;
+        result = 31 * result + deviceClass;
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "LutronDevice{" +
+                "id=" + id +
+                ", name='" + name + '\'' +
+                ", integrationId=" + integrationId +
+                ", linkAddress=" + linkAddress +
+                ", description='" + description + '\'' +
+                ", serialNumber=" + serialNumber +
+                ", deviceClass=" + deviceClass +
+                '}';
+    }
+
+    public boolean hasUpdatedProperties() {
+        return !properties.isEmpty();
+    }
+
+    public void putProperty(int pnum, int pval) {
+        lastUpdated = System.currentTimeMillis();
+        properties.put(pnum, pval);
+    }
+}

          
A => src/main/java/org/openhab/binding/lutronmqtt/model/LutronDeviceState.java +22 -0
@@ 0,0 1,22 @@ 
+package org.openhab.binding.lutronmqtt.model;
+
+public class LutronDeviceState {
+    protected Boolean powered;
+    protected float level;
+
+    public Boolean getPowered() {
+        return powered;
+    }
+
+    public void setPowered(Boolean powered) {
+        this.powered = powered;
+    }
+
+    public float getLevel() {
+        return level;
+    }
+
+    public void setLevel(float level) {
+        this.level = level;
+    }
+}
  No newline at end of file