/**
 *  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);
}