# HG changeset patch # User William Welliver # Date 1544499502 18000 # Mon Dec 10 22:38:22 2018 -0500 # Node ID c01cdd7e86dfb4fc02a819d073c252a533b2308f # Parent 0000000000000000000000000000000000000000 initial commit of lutron-mqtt (hacked wink hub) binding diff --git a/.hgignore b/.hgignore new file mode 100644 --- /dev/null +++ b/.hgignore @@ -0,0 +1,5 @@ +target +.idea +.classpath +.project +.iml$ diff --git a/ESH-INF/binding/binding.xml b/ESH-INF/binding/binding.xml new file mode 100644 --- /dev/null +++ b/ESH-INF/binding/binding.xml @@ -0,0 +1,11 @@ + + + + LutronMQTT Binding + This is the binding for LutronMQTT. + William Welliver + + diff --git a/ESH-INF/i18n/lutronmqtt_xx_XX.properties b/ESH-INF/i18n/lutronmqtt_xx_XX.properties new file mode 100644 --- /dev/null +++ b/ESH-INF/i18n/lutronmqtt_xx_XX.properties @@ -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 = +binding.lutronmqtt.description = + +# thing types +thing-type.lutronmqtt.sample.label = +thing-type.lutronmqtt.sample.description = + +# channel types +channel-type.lutronmqtt.sample-channel.label = +channel-type.lutronmqtt.sample-channel.description = \ No newline at end of file diff --git a/ESH-INF/thing/hub.xml b/ESH-INF/thing/hub.xml new file mode 100644 --- /dev/null +++ b/ESH-INF/thing/hub.xml @@ -0,0 +1,28 @@ + + + + + + + The Lutron-MQTT Gateway represents the network device communicating with the Lutron ClearConnect network. + + + Hacienda + + + + + + + + The token authorized by the luton-mqtt infrastructure + true + + + + + + \ No newline at end of file diff --git a/ESH-INF/thing/thing-types.xml b/ESH-INF/thing/thing-types.xml new file mode 100644 --- /dev/null +++ b/ESH-INF/thing/thing-types.xml @@ -0,0 +1,96 @@ + + + + + + + + + + Controls dimmable loads + + + + + + + + + + + + + + + + + + + + Controls variable speed fans + + + + + + + + + + + + + + + + Dimmer + + Increase/decrease the light level + DimmableLight + + + + + Switch + + Turn the light on or off + DimmableLight + + + + Dimmer + + Increase/decrease the speed + VariableFan + + + + + Switch + + Turn the item on or off + VariableFan + + + + + + + Sample thing for LutronMQTT Binding + + + + + + + + + This is a sample text configuration parameter. + + + + + + diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -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 diff --git a/OSGI-INF/.gitignore b/OSGI-INF/.gitignore new file mode 100644 --- /dev/null +++ b/OSGI-INF/.gitignore @@ -0,0 +1,1 @@ +*.xml \ No newline at end of file diff --git a/OSGI-INF/org.openhab.binding.lutronmqtt.discovery.LutronMQTTHubDiscoveryParticipant.xml b/OSGI-INF/org.openhab.binding.lutronmqtt.discovery.LutronMQTTHubDiscoveryParticipant.xml new file mode 100644 --- /dev/null +++ b/OSGI-INF/org.openhab.binding.lutronmqtt.discovery.LutronMQTTHubDiscoveryParticipant.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/OSGI-INF/org.openhab.binding.lutronmqtt.internal.LutronMQTTHandlerFactory.xml b/OSGI-INF/org.openhab.binding.lutronmqtt.internal.LutronMQTTHandlerFactory.xml new file mode 100644 --- /dev/null +++ b/OSGI-INF/org.openhab.binding.lutronmqtt.internal.LutronMQTTHandlerFactory.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/README.md b/README.md new file mode 100644 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# 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 ```.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!_ diff --git a/about.html b/about.html new file mode 100644 --- /dev/null +++ b/about.html @@ -0,0 +1,32 @@ + + + + + About + + +

About This Content

+ +

March 30, 2017

+

License

+ +

+ The openHAB community makes available all content in this plug-in ("Content"). Unless otherwise + indicated below, the Content is provided to you under the terms and conditions of the + Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available + at http://www.eclipse.org/legal/epl-v10.html. + For purposes of the EPL, "Program" will mean the Content. +

+ +

+ If you did not receive this Content directly from the openHAB community, the Content is + being redistributed by another party ("Redistributor") 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 openhab.org. +

+ + + diff --git a/build.properties b/build.properties new file mode 100644 --- /dev/null +++ b/build.properties @@ -0,0 +1,6 @@ +source..=src/main/java/ +output..=target/classes +bin.includes=META-INF/,\ + .,\ + OSGI-INF/,\ + ESH-INF/ diff --git a/pom.xml b/pom.xml new file mode 100644 --- /dev/null +++ b/pom.xml @@ -0,0 +1,166 @@ + + + + 4.0.0 + + + org.eclipse.smarthome.config + org.eclipse.smarthome.config.discovery.mdns + 0.9.0-SNAPSHOT + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.0.2 + + + + + org.openhab + pom-tycho + 2.4.0-SNAPSHOT + + + + + false + 2.4.0-SNAPSHOT + + + org.openhab.binding.lutronmqtt + + LutronMQTT Binding + eclipse-plugin + + + src/test + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.19.1 + + + test + test + + + **/*Test.java + + + + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 1.7 + 1.7 + + + + compiletests + test-compile + + testCompile + + + + + + + + + + jcenter + JCenter Repository + https://jcenter.bintray.com/ + + true + never + + + false + + + + artifactory + JFrog Artifactory Repository + https://openhab.jfrog.io/openhab/libs-release + + true + never + + + false + + + + + + + + + jcenter + JCenter Repository + https://jcenter.bintray.com/ + + true + never + + + false + + + + + openhab-artifactory-release + JFrog Artifactory Repository + https://openhab.jfrog.io/openhab/libs-release + + true + never + + + false + + + + + + openhab-artifactory-snapshot + JFrog Artifactory Repository + https://openhab.jfrog.io/openhab/libs-snapshot + + false + + + true + always + + + + + + p2-smarthome + https://openhab.jfrog.io/openhab/eclipse-smarthome-stable + p2 + + + + + p2-openhab-deps-repo + https://dl.bintray.com/openhab/p2/openhab-deps-repo/${ohdr.version} + p2 + + + + + diff --git a/src/main/java/org/openhab/binding/lutronmqtt/LutronMQTTBindingConstants.java b/src/main/java/org/openhab/binding/lutronmqtt/LutronMQTTBindingConstants.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/LutronMQTTBindingConstants.java @@ -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 SUPPORTED_HUBS_UIDS = ImmutableSet.of(THING_TYPE_MQTTHUB); + + public final static Set 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; +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTDeviceDiscoveryService.java b/src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTDeviceDiscoveryService.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTDeviceDiscoveryService.java @@ -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 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 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 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 + } +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTHubDiscoveryParticipant.java b/src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTHubDiscoveryParticipant.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/discovery/LutronMQTTHubDiscoveryParticipant.java @@ -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 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 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 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; + } +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/handler/DeviceStatusListener.java b/src/main/java/org/openhab/binding/lutronmqtt/handler/DeviceStatusListener.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/handler/DeviceStatusListener.java @@ -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); + +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/handler/LightStateConverter.java b/src/main/java/org/openhab/binding/lutronmqtt/handler/LightStateConverter.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/handler/LightStateConverter.java @@ -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 toLightState(OnOffType onOffType, LutronDevice device) { + int level = toOnOffLightState(onOffType); + Map m = makeGoToLevelCommand(level, device); + return m; + } + + public static Map makeGoToLevelCommand(int level, LutronDevice device) { + Map 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 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 toLightState(PercentType percentType, LutronDevice device) { + int level = toPercentLightState(percentType); + + Map m = makeGoToLevelCommand(level, device); + + return m; + } + + public static Map toLightState(IncreaseDecreaseType increaseDecreaseType, LutronDevice device) { + int level = toAdjustedBrightness(increaseDecreaseType, device.getProperty(LUTRON_PROPERTY_LEVEL)); + + Map 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; + } + +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTDimmableLightHandler.java b/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTDimmableLightHandler.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTDimmableLightHandler.java @@ -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); + } + +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTHubHandler.java b/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTHubHandler.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTHubHandler.java @@ -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 deviceStatusListeners = new HashSet<>(); + + private List deviceList = Collections.EMPTY_LIST; + + @Nullable + private LutronMQTTConfiguration config; + private String token; + private MqttClient mqttClient; + private Gson gson = new Gson(); + private Map 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 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 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 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 devices = new ArrayList<>(); + for(Map objectMap : (List>)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> l = (List>)(((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 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 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 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(); + } + } +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTRemoteHandler.java b/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTRemoteHandler.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTRemoteHandler.java @@ -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) { + + } +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTVariableFanHandler.java b/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTVariableFanHandler.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/handler/LutronMQTTVariableFanHandler.java @@ -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); + } +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/handler/PowerLevelDeviceHandler.java b/src/main/java/org/openhab/binding/lutronmqtt/handler/PowerLevelDeviceHandler.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/handler/PowerLevelDeviceHandler.java @@ -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 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 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; + } + +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTConfiguration.java b/src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTConfiguration.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTConfiguration.java @@ -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; +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTHandlerFactory.java b/src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTHandlerFactory.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/internal/LutronMQTTHandlerFactory.java @@ -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 SUPPORTED_THING_TYPES_UIDS = LutronMQTTBindingConstants.SUPPORTED_THING_TYPES_UIDS; + private Map> 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())); + } + + @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()); + } + } + } + +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/model/LutronDevice.java b/src/main/java/org/openhab/binding/lutronmqtt/model/LutronDevice.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/model/LutronDevice.java @@ -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 properties = new HashMap<>(); + private long lastUpdated; + + public LutronDevice(Map 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); + } +} diff --git a/src/main/java/org/openhab/binding/lutronmqtt/model/LutronDeviceState.java b/src/main/java/org/openhab/binding/lutronmqtt/model/LutronDeviceState.java new file mode 100644 --- /dev/null +++ b/src/main/java/org/openhab/binding/lutronmqtt/model/LutronDeviceState.java @@ -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