/** * OpenHabAppV2 * * Description * Provides two way communications between a Smartthings Hub and OpenHAB * Messages from OpenHAB with the following paths are supported and perform the following functions * /state - returns the state of the specified device and attribute, i.e. on, off, 95 * /update - Updates the state of the specified device and attribute * /discovery - Returns a list of the devices * /error - Returns error messages to OpenHAB for logging * Messages are sent to OpenHAB with the following paths * /smartthings/push - When an event occurs on a monitored device the new value is sent to OpenHAB * * Authors * - rjraker@gmail.com - 1/30/17 - Modified for use with Smartthings * - st.john.johnson@gmail.com and jeremiah.wuenschel@gmail.com- original code for interface with another device * * Copyright 2016 - 2021 * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. */ import groovy.json.JsonSlurper import groovy.json.JsonOutput import groovy.json.JsonBuilder import groovy.transform.Field // Massive lookup tree @Field CAPABILITY_MAP = [ "accelerationSensor": [ name: "Acceleration Sensor", capability: "capability.accelerationSensor", attributes: [ "acceleration" ] ], "airConditionerMode": [ name: "Air Conditioner Mode", capability: "capability.airConditionerMode", attributes: [ "airConditionerMode" ], action: actionAirConditionerMode ], "alarm": [ name: "Alarm", capability: "capability.alarm", attributes: [ "alarm" ], action: "actionAlarm" ], "battery": [ name: "Battery", capability: "capability.battery", attributes: [ "battery" ] ], "beacon": [ name: "Beacon", capability: "capability.beacon", attributes: [ "presence" ] ], "bulb": [ name: "Bulb", capability: "capability.bulb", attributes: [ "switch" ], action: "actionOnOff" ], "button": [ name: "Button", capability: "capability.button", attributes: [ "button" ] ], "carbonDioxideMeasurement": [ name: "Carbon Dioxide Measurement", capability: "capability.carbonDioxideMeasurement", attributes: [ "carbonDioxide" ] ], "carbonMonoxideDetector": [ name: "Carbon Monoxide Detector", capability: "capability.carbonMonoxideDetector", attributes: [ "carbonMonoxide" ] ], "colorControl": [ name: "Color Control", capability: "capability.colorControl", attributes: [ "hue", "saturation", "color" ], action: "actionColorControl" ], "color": [ name: "Color (proposed)", capability: "capability.color", attributes: [ "colorValue" ], action: "actionColor" ], "colorTemperature": [ name: "Color Temperature", capability: "capability.colorTemperature", attributes: [ "colorTemperature" ], action: "actionColorTemperature" ], "consumable": [ name: "Consumable", capability: "capability.consumable", attributes: [ "consumable" ], action: "actionConsumable" ], "contactSensor": [ name: "Contact Sensor", capability: "capability.contactSensor", attributes: [ "contact" ] ], "doorControl": [ name: "Door Control", capability: "capability.doorControl", attributes: [ "door" ], action: "actionOpenClosed" ], "energyMeter": [ name: "Energy Meter", capability: "capability.energyMeter", attributes: [ "energy" ] ], "dryerMode": [ name: "Dryer Mode", capability: "capability.dryerMode", attributes: [ "dryerMode" ], action: "actionApplianceMode" ], "dryerOperatingState": [ name: "Dryer Operating State", capability: "capability.dryerOperatingState", attributes: [ "machineState", "dryerJobState" ], action: "actionMachineState" ], "estimatedTimeOfArrival": [ name: "Estimated Time Of Arrival", capability: "capability.estimatedTimeOfArrival", attributes: [ "eta" ] ], "garageDoorControl": [ name: "Garage Door Control", capability: "capability.garageDoorControl", attributes: [ "door" ], action: "actionOpenClosed" ], "holdableButton": [ name: "Holdable Button", capability: "capability.holdableButton", attributes: [ "button", "numberOfButtons" ], action: "actionOpenClosed" ], "illuminanceMeasurement": [ name: "Illuminance Measurement", capability: "capability.illuminanceMeasurement", attributes: [ "illuminance" ] ], "imageCapture": [ name: "Image Capture", capability: "capability.imageCapture", attributes: [ "image" ] ], "indicator": [ name: "Indicator", capability: "capability.indicator", attributes: [ "indicatorStatus" ], action: indicator ], "infraredLevel": [ name: "Infrared Level", capability: "capability.infraredLevel", attributes: [ "infraredLevel" ], action: "actionLevel" ], "lock": [ name: "Lock", capability: "capability.lock", attributes: [ "lock" ], action: "actionLock" ], "lockOnly": [ name: "Lock Only", capability: "capability.lockOnly", attributes: [ "lock" ], action: "actionLockOnly" ], "mediaController": [ name: "Media Controller", capability: "capability.mediaController", attributes: [ "activities", "currentActivity" ] ], "motionSensor": [ name: "Motion Sensor", capability: "capability.motionSensor", attributes: [ "motion" ], action: "actionActiveInactive" ], "musicPlayer": [ name: "Music Player", capability: "capability.musicPlayer", attributes: [ "status", "level", "trackDescription", "trackData", "mute" ], action: "actionMusicPlayer" ], "outlet": [ name: "Outlet", capability: "capability.outlet", attributes: [ "switch" ], action: "actionOnOff" ], "pHMeasurement": [ name: "pH Measurement", capability: "capability.pHMeasurement", attributes: [ "pH" ] ], "powerMeter": [ name: "Power Meter", capability: "capability.powerMeter", attributes: [ "power" ] ], "powerSource": [ name: "Power Source", capability: "capability.powerSource", attributes: [ "powerSource" ] ], "presenceSensor": [ name: "Presence Sensor", capability: "capability.presenceSensor", attributes: [ "presence" ] ], "relativeHumidityMeasurement": [ name: "Relative Humidity Measurement", capability: "capability.relativeHumidityMeasurement", attributes: [ "humidity" ] ], "relaySwitch": [ name: "Relay Switch", capability: "capability.relaySwitch", attributes: [ "switch" ], action: "actionOnOff" ], "shockSensor": [ name: "Shock Sensor", capability: "capability.shockSensor", attributes: [ "shock" ] ], "signalStrength": [ name: "Signal Strength", capability: "capability.signalStrength", attributes: [ "lqi", "rssi" ] ], "sleepSensor": [ name: "Sleep Sensor", capability: "capability.sleepSensor", attributes: [ "sleeping" ] ], "smokeDetector": [ name: "Smoke Detector", capability: "capability.smokeDetector", attributes: [ "smoke", "carbonMonoxide" ] ], "soundPressureLevel": [ name: "Sound Pressure Level", capability: "capability.soundPressureLevel", attributes: [ "soundPressureLevel" ] ], "soundSensor": [ name: "Sound Sensor", capability: "capability.soundSensor", attributes: [ "phraseSpoken" ] ], "speechRecognition": [ name: "Speech Recognition", capability: "capability.speechRecognition", action: [ "speak" ] ], "stepSensor": [ name: "Step Sensor", capability: "capability.stepSensor", attributes: [ "steps", "goal" ] ], "switch": [ name: "Switch", capability: "capability.switch", attributes: [ "switch" ], action: "actionOnOff" ], "switchLevel": [ name: "Dimmer Switch", capability: "capability.switchLevel", attributes: [ "level" ], action: "actionLevel" ], "soundPressureLevel": [ name: "Sound Pressure Level", capability: "capability.soundPressureLevel", attributes: [ "soundPressureLevel" ] ], "tamperAlert": [ name: "Tamper Alert", capability: "capability.tamperAlert", attributes: [ "tamper" ] ], "temperatureMeasurement": [ name: "Temperature Measurement", capability: "capability.temperatureMeasurement", attributes: [ "temperature" ] ], "thermostat": [ name: "Thermostat", capability: "capability.thermostat", attributes: [ "temperature", "heatingSetpoint", "coolingSetpoint", "thermostatSetpoint", "thermostatMode", "thermostatFanMode", "thermostatOperatingState" ], action: "actionThermostat" ], "thermostatCoolingSetpoint": [ name: "Thermostat Cooling Setpoint", capability: "capability.thermostatCoolingSetpoint", attributes: [ "coolingSetpoint" ], action: "actionThermostat" ], "thermostatFanMode": [ name: "Thermostat Fan Mode", capability: "capability.thermostatFanMode", attributes: [ "thermostatFanMode" ], action: "actionThermostat" ], "thermostatHeatingSetpoint": [ name: "Thermostat Heating Setpoint", capability: "capability.thermostatHeatingSetpoint", attributes: [ "heatingSetpoint" ], action: "actionThermostat" ], "thermostatMode": [ name: "Thermostat Mode", capability: "capability.thermostatMode", attributes: [ "thermostatMode" ], action: "actionThermostat" ], "thermostatOperatingState": [ name: "Thermostat Operating State", capability: "capability.thermostatOperatingState", attributes: [ "thermostatOperatingState" ] ], "thermostatSetpoint": [ name: "Thermostat Setpoint", capability: "capability.thermostatSetpoint", attributes: [ "thermostatSetpoint" ] ], "threeAxis": [ name: "Three Axis", capability: "capability.threeAxis", attributes: [ "threeAxis" ] ], "timedSession": [ name: "Timed Session", capability: "capability.timedSession", attributes: [ "timeRemaining", "sessionStatus" ], action: "actionTimedSession" ], "touchSensor": [ name: "Touch Sensor", capability: "capability.touchSensor", attributes: [ "touch" ] ], "valve": [ name: "Valve", capability: "capability.valve", attributes: [ "valve" ], action: "actionOpenClosed" ], "voltageMeasurement": [ name: "Voltage Measurement", capability: "capability.voltageMeasurement", attributes: [ "voltage" ] ], "washerMode": [ name: "Washer Mode", capability: "capability.washerMode", attributes: [ "washerMode" ], action: "actionApplianceMode" ], "washerOperatingState": [ name: "Washer Operating State", capability: "capability.washerOperatingState", attributes: [ "machineState", "washerJobState" ], action: "actionMachineState" ], "waterSensor": [ name: "Water Sensor", capability: "capability.waterSensor", attributes: [ "water" ] ], "windowShade": [ name: "Window Shade", capability: "capability.windowShade", attributes: [ "windowShade" ], action: "actionOpenClosed" ] ] definition( name: "OpenHabAppV2", namespace: "bobrak", author: "Bob Raker", description: "Provides two way communications between a Smartthings Hub and OpenHAB", category: "My Apps", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections@2x.png", iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Connections/Cat-Connections@3x.png" ) preferences { section("Send Notifications?") { input("recipients", "contact", title: "Send notifications to", multiple: true, required: false) } section ("Input") { CAPABILITY_MAP.each { key, capability -> input key, capability["capability"], title: capability["name"], description: capability["key"], multiple: true, required: false } } section ("Device") { input "openhabDevice", "capability.notification", title: "Notify this virtual device", required: true, multiple: false } } def installed() { log.debug "Installed with settings: ${settings}" initialize() } def updated() { log.debug "Updated with settings: ${settings}" // Unsubscribe from all events unsubscribe() // Subscribe to stuff initialize() } def initialize() { // Subscribe to new events from devices CAPABILITY_MAP.each { key, capability -> capability["attributes"].each { attribute -> if ( settings[key] != null ) { subscribe(settings[key], attribute, inputHandler) log.debug "Subscribing inputHandler to device \"${settings[key]}\" with attribute \"${attribute}\"" } } } // Subscribe to events from the openhabDevice log.debug "Subscribing to event handler ${openHabDevice}" subscribe(openhabDevice, "message", openhabMessageHandler) } // Receive an event from OpenHAB via the openhabDevice def openhabMessageHandler(evt) { def json = new JsonSlurper().parseText(evt.value) log.debug "Received device event from Message : ${json}" switch (json.path) { case "update": openhabUpdateHandler (evt) break case "state": openhabStateHandler (evt) break case "discovery": openhabDiscoveryHandler (evt) break default: log.debug "Received device event from Message **** UNEXPECTED **** : ${json}" } } // Handler for "current" state requests def openhabStateHandler(evt) { def mapIn = new JsonSlurper().parseText(evt.value) log.debug "Received state event from openhabDevice: ${mapIn}" // Get the CAPABILITY_MAP entry for this device type def capability = CAPABILITY_MAP[mapIn.capabilityKey] if (capability == null) { log.error "No capability: \"${mapIn.capabilityKey}\" exists, make sure there is a CAPABILITY_MAP entry for this capability." sendErrorResponse "Requested current state information for CAPABILITY: \"${mapIn.capabilityKey}\" but this is not defined in the SmartApp" return } // Verify the attribute is on this capability if (! capability.attributes.contains(mapIn.capabilityAttribute) ) { log.error "Capability \"${mapIn.capabilityKey}\" does NOT contain the expected attribute: \"${mapIn.capabilityAttribute}\", make sure the a CAPABILITY_MAP for this capability contains the missing attribte." sendErrorResponse "Requested current state information for CAPABILITY: \"${mapIn.capabilityKey}\" with attribute: \"${mapIn.capabilityAttribute}\" but this is attribute not defined for this capability in the SmartApp" return } // Look for the device associated with this capability and return the value of the specified attribute settings[mapIn.capabilityKey].each {device -> if (device.displayName == mapIn.deviceDisplayName) { // Have the device, get the value and return the correct message def currentState = device.currentValue(mapIn.capabilityAttribute) // Have to handle special values. Ones that are not numeric or string // This switch statement should just be considered a beginning. There are other cases that I dont have devices to test def capabilityAttr = mapIn.capabilityAttribute switch (capabilityAttr) { case 'threeAxis' : currentState = "${currentState}" break default : break } def jsonOut = new JsonOutput().toJson([ path: "/smartthings/state", body: [ deviceDisplayName: device.displayName, capabilityAttribute: capabilityAttr, value: currentState ] ]) log.debug "State Handler is returning ${jsonOut}" openhabDevice.deviceNotification(jsonOut) } } } // Update a device when requested from OpenHAB def openhabUpdateHandler(evt) { def json = new JsonSlurper().parseText(evt.value) // log.debug "Received update event from openhabDevice: ${json}" // printSettings() if (json.type == "notify") { if (json.name == "Contacts") { sendNotificationToContacts("${json.value}", recipients) } else { sendNotificationEvent("${json.value}") } return } // Get the CAPABILITY_MAP entry for this device type def capability = CAPABILITY_MAP[json.capabilityKey] if (capability == null) { //log.error "No capability: \"${json.capabilityKey}\" exists, make sure there is a CAPABILITY_MAP entry for this capability." sendErrorResponse "Update failed device displayName of: \"${json.deviceDisplayName}\" with CAPABILITY: \"${json.capabilityKey}\" because that CAPABILTY does not exist in the SmartApp" return } // Look for the device associated with this capability and perform the requested action settings[json.capabilityKey].each {device -> // log.debug "openhabUpdateHandler - looking at devices with capabilityKey ${json.capabilityKey} and device{ ${device.displayName}." if (device.displayName == json.deviceDisplayName) { log.debug "openhabUpdateHandler - found device for ${json.deviceDisplayName}" if (capability.containsKey("action")) { // log.debug "openhabUpdateHandler - Capability ${capability.name} with device name ${device.displayName} changed to ${json.value} using action ${capability.action}" def action = capability["action"] // Yes, this is calling the method dynamically try { "$action"(device, json.capabilityAttribute, json.value) } catch (e) { sendErrorResponse "Error occured while calling action: {$action} for Capability: ${capability.name} with device name: ${device.displayName} changed to: ${json.value}. Exception ${e}" // log.error "Error occured while calling action: {$action} for Capability: ${capability.name} with device name: ${device.displayName} changed to: ${json.value}. Exception ${e}" } } } } } // Debug method def printSettings() { log.debug "**** printSettings() ****" String out settings.each { key, device -> out += " *** ${key} *** \n" device.each { d -> out += "[ key: ${key}, deviceName: ${d.name}, deviceLabel: ${d.label}, deviceValue: ${d.currentValue} " /* The following does work for showing attributes bug significantly expands the output def attributes = d.getSupportedAttributes() out += ", attrLen: ${attributes.size()}" attributes.each { a-> out += ", ${a}" } out += "], \n" */ } } log.debug "*** printSettings() done ***" } def sendErrorResponse (msg) { def jsonOut = new JsonOutput().toJson([ path: "/smartthings/error", body: [ message: msg ] ]) openhabDevice.deviceNotification(jsonOut) log.error msg } // Send a list of all devices to OpenHAB - used during OpenHAB's discovery process // The hub is only capable of sending back a buffer of ~40,000 bytes. This routine // will send multiple responses anytime the buffer exceeds 30,000 bytes def openhabDiscoveryHandler(evt) { def mapIn = new JsonSlurper().parseText(evt.value) log.debug "Entered discovery handler with mapIn: ${mapIn}" def results = [] def bufferLength = 0 def deviceCount = 0 CAPABILITY_MAP.each { key, capability -> capability["attributes"].each { attribute -> settings[key].each {device -> // The device info has to be returned as a string. It will be parsed into device data on the OpenHAB side def deviceInfo = "{\"capability\": \"${key}\", \"attribute\": \"${attribute}\", \"name\": \"${device.displayName}\", \"id\": \"${device.id}\" }" results.push(deviceInfo) deviceCount++ bufferLength += deviceInfo.length() // Check if we have close to a full buffer and if so send it if( bufferLength > 30000 ) { def json = new groovy.json.JsonOutput().toJson([ path: "/smartthings/discovery", body: results ]) log.debug "Discovery is returning JSON: ${json}" openhabDevice.deviceNotification(json) results = [] bufferLength = 0 } } } } if( bufferLength > 0 ) { def json = new groovy.json.JsonOutput().toJson([ path: "/smartthings/discovery", body: results ]) log.debug "Discovery is returning FINAL JSON: ${json}" openhabDevice.deviceNotification(json) } log.debug "Discovery returned data for ${deviceCount} devices." } // Receive an event from a device and send it onto OpenHAB def inputHandler(evt) { def device = evt.device def capabilities = device.capabilities def json = new JsonOutput().toJson([ path: "/smartthings/state", body: [ deviceDisplayName: evt.displayName, value: evt.value, capabilityAttribute: evt.name, ] ]) log.debug "Forwarding device event to openhabDevice: ${json}" openhabDevice.deviceNotification(json) } // +---------------------------------+ // | WARNING, BEYOND HERE BE DRAGONS | // +---------------------------------+ // These are the functions that handle incoming messages from OpenHAB. // I tried to put them in closures but apparently SmartThings Groovy sandbox // restricts you from running closures from an object (it's not safe). // This handles the basic case where there is one attribute and one action that sets the attribute. // And, the value is always an ENUM def actionEnum(device, attribute, value) { log.debug "actionEnum: Setting device \"${device}\" with attribute \"${attribute}\" to value \"${value}\"" //device."${value}"() // I can't figure out why this doesn't work, but it doesn't def converted = "set" + attribute.capitalize() device."$converted"(value) } def actionAirConditionerMode(device, attribute, value) { log.debug "actionAirConditionerMode: Setting device \"${device}\" with attribute \"${attribute}\" to value \"${value}\"" device.setAirConditionerMode(value) } def actionAlarm(device, attribute, value) { switch (value) { case "strobe": device.strobe() break case "siren": device.siren() break case "off": device.off() break case "both": device.both() break } } // This is the original color control // 1-19-2021 The color values of on and off were added in response to issue https://github.com/BobRak/OpenHAB-Smartthings/issues/102 // These changes were made because OH 3.0 uses color values of on/off. OH 2 and 3.1 don't seem to need this. def actionColorControl(device, attribute, value) { log.debug "actionColor: attribute \"${attribute}\", value \"${value}\"" switch (attribute) { case "hue": device.setHue(value as int) break case "saturation": device.setSaturation(value as int) break case "color": if (value == "off") { device.off() } else if (value == "on") { device.on() } else { def colormap = ["hue": value[0] as int, "saturation": value[1] as int] log.debug "actionColorControl: Setting device \"${device}\" with attribute \"${attribute}\" to colormap \"${colormap}\"" device.setColor(colormap) device.setLevel(value[2] as int) } break } } // This is the new "proposed" color. Here hue is 0-360 // 1-19-2021 The attributes of on and off were added in response to issue https://github.com/BobRak/OpenHAB-Smartthings/issues/102 // These changes were made because OH 3.0 uses color values of on/off. OH 2 and 3.1 don't seem to need this. def actionColor(device, attribute, value) { log.debug "actionColor: attribute \"${attribute}\", value \"${value}\"" switch (attribute) { case "hue": device.setHue(value as int) break case "saturation": device.setSaturation(value as int) break case "colorValue": def colormap = ["hue": value[0] as int, "saturation": value[1] as int] // log.debug "actionColor: Setting device \"${device}\" with attribute \"${attribute}\" to colormap \"${colormap}\"" device.setColor(colormap) device.setLevel(value[2] as int) break case "off": // log.debug "actionColor: Setting device \"${device}\" with attribute \"${attribute}\" to off" device.off() break case "on": // log.debug "actionColor: Setting device \"${device}\" with attribute \"${attribute}\" to on" device.on() break } } def actionOpenClosed(device, attribute, value) { if (value == "open") { device.open() } else if (value == "close") { device.close() } } def actionOnOff(device, attribute, value) { if (value == "off") { device.off() } else if (value == "on") { device.on() } } def actionActiveInactive(device, attribute, value) { if (value == "active") { device.active() } else if (value == "inactive") { device.inactive() } } def actionThermostat(device, attribute, value) { log.debug "actionThermostat: Setting device \"${device}\" with attribute \"${attribute}\" to value \"${value}\"" switch(attribute) { case "heatingSetpoint": device.setHeatingSetpoint(value) break case "coolingSetpoint": device.setCoolingSetpoint(value) break case "thermostatMode": device.setThermostatMode(value) break case "thermostatFanMode": device.setThermostatFanMode(value) break } } def actionMusicPlayer(device, attribute, value) { switch(attribute) { case "level": device.setLevel(value) break case "mute": if (value == "muted") { device.mute() } else if (value == "unmuted") { device.unmute() } break } } def actionColorTemperature(device, attribute, value) { device.setColorTemperature(value as int) } def actionLevel(device, attribute, value) { //log.debug "actionLevel: Setting device \"${device}\" with attribute \"${attribute}\" to value \"${value}\"" // OpenHAB will send on / off or a number for the percent. See what we got and acct accordingly if (value == "off") { device.off() } else if (value == "on") { device.on() } else { device.setLevel(value as int) // And, set the switch to on if level > 0 otherwise off if( value > 0 ) { device.on() } else { device.off() } } } def actionConsumable(device, attribute, value) { device.setConsumableStatus(value) } def actionLock(device, attribute, value) { // log.debug "actionLock: Setting device \"${device}\" with attribute \"${attribute}\" to value \"${value}\"" if (value == "locked") { device.lock() } else if (value == "unlocked") { device.unlock() } } def actionLockOnly(device, attribute, value) { // log.debug "actionLockOnly: Setting device \"${device}\" with attribute \"${attribute}\" to value \"${value}\"" if (value == "locked") { device.lock() } } def actionTimedSession(device, attribute, value) { if (attribute == "timeRemaining") { device.setTimeRemaining(value) } } def actionApplianceMode(device, attribute, value) { //log.debug "actionDryeMode: attribute: ${attribute} value: ${value}" // Through trial and error I figured out that changing the dryerMode requires the following code // Originally this was called actionDryerMode but then the washer was added I renamed and added washer modes switch (value) { // Modes used by both washer and dryer case "regular": device.regular() break // Dryer modes case "lowHeat": device.lowHeat() break case "highHeat": device.highHeat() break // washer modes case "heavy": device.heavy() break case "rinse": device.rinse() break case "spinDry": device.spinDry() break } } def actionMachineState(device, attribute, value) { //log.debug "actionMachineState: attribute: ${attribute} value: ${value}" // Through trial and error I figured out that changing the machineState requires the following code switch (value) { case "run": device.start() break case "stop": device.stop() break case "pause": device.pause() break // I'm not sure if unpause() is valid. I saw an error message that included unpause as a valid command but it is not included in the Capabilities for MachineState case "unpause": device.unpause() break } } // The following functions return the current state of a device def switchState(device, attribute) { device.currentValue(attribute); }