/**
* SmartThings Lightify Dimmer Switch support
* Copyright (C) 2016 Adam Outler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
/**
* Command reference
* on 'catchall: 0104 0006 01 01 0140 00 3A68 01 00 0000 01 00 '
* off 'catchall: 0104 0006 01 01 0140 00 3A68 01 00 0000 00 00 '
* held up 'catchall: 0104 0008 01 01 0140 00 3A68 01 00 0000 05 00 0032'
* held down 'catchall: 0104 0008 01 01 0140 00 3A68 01 00 0000 01 00 0132'
* released 'catchall: 0104 0008 01 01 0140 00 3A68 01 00 0000 03 00 '
*/
/**
*sets up fingerprint for autojoin
*sets up commands for polling and others
*sets up capabilities
*sets up variables
*/
metadata {
definition(name: "Lightify Dimming Switch- Zigbee", namespace: "adamoutler", author: "Adam Amber House") {
capability "Battery"
capability "Button"
capability "Switch Level"
attribute "State Array", "string"
attribute 'Awesomeness Level', "string"
attribute 'state', "string"
command "refresh"
command "poll"
command "toggle"
command "configure"
command "installed"
fingerprint profileId: "0104", deviceId: "0001", inClusters: "0000, 0001, 0003, 0020, 0402, 0B05", outClusters: "0003, 0006, 0008, 0019", manufacturer: "OSRAM", model: "LIGHTIFY Dimming Switch", deviceJoinName: "OSRAM Lightify Dimming Switch"
}
simulator {
// Simulations are for loosers, work in production :D
}
preferences {
section("Device1") {
input("device1", "string", title: "Device Network ID 1", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
input("end1", "string", title: "Device Endpoint ID 1", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
}
section("Device2") {
input("device2", "string", title: "Device Network ID 2", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
input("end2", "string", title: "Device Endpoint ID 2", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
}
section("Device3") {
input("device3", "string", title: "Device Network ID 3", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
input("end3", "string", title: "Device Endpoint ID 3", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
}
section("Device4") {
input("device4", "string", title: "Device Network ID 4", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
input("end4", "string", title: "Device Endpoint ID 4", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
}
section("Device5") {
input("device5", "string", title: "Device Network ID 5", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
input("end5", "string", title: "Device Endpoint ID 5", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
}
}
/**
*UI tile definitions
*/
tiles(scale: 2) {
standardTile("button", "device.state", width: 6, height: 4) {
state "off", label: 'Off', action: "toggle", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
state "on", label: "On", action: "toggle", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
state "turningOn", label: 'Turning on', icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
state "turningOff", label: 'Turning off', icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
}
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "battery", label: 'battery ${currentValue}%'
}
valueTile("brightness", "device.level", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
state "brightness", label: 'brightnessn${currentValue}%'
}
standardTile("refresh", "device.button", decoration: "flat", width: 2, height: 2) {
state "default", label: "", action: "refresh", icon: "st.secondary.refresh"
}
controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 6, inactiveLabel: false, range:"(1..100)") {
state "level", action:"setLevel"
}
main "button"
details(["button", "levelSliderControl", "switch", "battery", "brightness", "refresh"])
}
}
/**
*returns a map representing important states
*/
private Map getStatus() {
return [name: 'button',
brightness: state.brightness,
battery: state.battery,
value: state.value,
level: state.level,
lastAction: state.lastAction,
on: state.on,
data: '',
descriptionText: "$device.displayName button $state.buttonNumber was $value"
]
}
/**
*Gets a list of devices in [address,endpoint] format compiled from inputs above
*returns: Arraylist<[address,endpoint]>
*/
final ArrayList < String[] > getDevices() {
String[] devs = [settings.device1, device2, device3, device4, device5]
if (devs == [null, null, null, null, null]) log.info("------No devices configured in $device preferences--------")
String[] ends = [end1, end2, end3, end4, end5]
ArrayList < String[] > list = new ArrayList < > ([])
for (int i = 0; i < 4; i++) {
if (devs[i] != null) {
list.add([devs[i], ends[i]])
}
}
return list
}
/**
*Parse events into attributes.
*/
def parse(String msgFromST) {
log.debug(msgFromST)
if (msgFromST?.startsWith('catchall:')) {
def value = handleMessage(msgFromST)
fireCommands(value.command)
} else if (msgFromST.startsWith("read")) {
if (msgFromST.contains("attrId: 0000")) return state
if (msgFromST.contains("attrId: 0020,")) return batteryHandler(zigbee.parseDescriptionAsMap(msgFromST))
log.error('unrecognized command:' + msgFromST)
} else {
log.error('unrecognized command:' + msgFromST)
}
return getStatus()
}
/**
*fire commands into the hub
*commands - an array of string commands to be fired
*/
private fireCommands(List commands) {
if (commands != null && commands.size() > 0) {
log.trace("Executing commands-- state:" + state + " commands:" + commands)
for (String value : commands){
sendHubCommand([value].collect {new physicalgraph.device.HubAction(it)
})
}
}
}
Map handleMessage(String msgFromST) {
Map msg = zigbee.parseDescriptionAsMap(msgFromST)
switch (Integer.parseInt(msg.clusterId)) {
case 6: //button press
def returnval = handleButtonPress(msg)
return returnval
break
case 8:
handleButtonHeld(msg)
break
case 8021:
log.info("Networking Bind Response received!!!")
updateButtonState("Network binding complete!")
state.boundnetwork=true
return state
break
case 8034:
log.info("Network managment Leave Response!!!")
state.boundnetwork=false
return state
break
default:
log.error("Unhandled message: " + msg)
break
}
}
/**
* This is the handler for button presses. Map is routed here once a button press has been detected
*/
def Map handleButtonPress(Map msg) {
switch (msg.command) {
case "01":
def returnval = on()
updateButtonState("on")
return returnval
break
case "03":
bothButtonsPressed()
break
case "00":
def returnval = off()
updateButtonState("off")
return returnval
break
case "07":
log.info("Button Press Bind Response!!!")
state.boundonoff=true
return state
break
default:
log.error(getLinkText(device)+" got unknown button press command: " + msg.command)
return [name:"error",value:"unknown button press command"]
break
}
}
/**
*this is the handler for button held events. Map is routed here once a button held event has been detected
*/
def Map handleButtonHeld(Map msg) {
switch (Integer.parseInt(msg.command)) {
case 1:
log.debug("Button held- Lowering brightness commanded")
state.dimming = true
updateButtonState("lowering brightness")
state.lastHeld = "down"
return adjustBrightness(false, state.brightness)
break
case 3:
log.debug("stop brightness commanded")
updateButtonState(state.lastHeld + " released")
state.dimming = false
return state
break
case 5:
log.debug("Button held- raising brightness commanded")
state.lastHeld = "up"
updateButtonState("raising brightness")
state.dimming = true
return adjustBrightness(true, state.brightness)
break
case 7:
log.info("Button Held Bind Response - 7!!!")
state.bounddimmer=true
return state
break
case 8:
log.info("Button Held Bind Response - 8!!!")
state.bounddimmer=true
return state
break
default:
log.error("Unhandled button held event: " + msg)
break
}
return msg
}
/**
* adjusts brightness up/down depending on the value of the up boolean true is up, false is down
* continues to adjust until state.dimming is changed
* up- true if we are adjusting brightness up, false if down
* level - first level commanded
*/
def Map adjustBrightness(final boolean up, double level) {
Map result
log.debug("adjusting brightness" + (up?"up" : "down") + " from current " + state.brightness)
if (state.dimming) {
//increase or decrease brightness
if (up) {
state.brightening = true
} else {
state.brightening = false
}
state.brightness = (int) level
executeBrightnessAdjustmentUntilButtonReleased()
} else {
log.debug("Final brightness adjusted to " + state.brightness)
}
sendEvent(name: "brightness", value: state.brightness)
return getStatus()
}
/**
* performs a recursive brightness adjustment based on state.brightening while state.dimming is true
*/
void executeBrightnessAdjustmentUntilButtonReleased() {
if (state.dimming) {
if (state.brightening) {
state.brightness = state.brightness + 20
} else {
state.brightness = state.brightness - 20
}
if (state.brightness > 100) {
state.brightness = 100
state.dimming=false
}
if (state.brightness < 1){
state.brightness = 1
state.dimming=false
}
setLevel((double) state.brightness, 1000)
reportOnState(true) //Manage and report states
runIn(1, executeBrightnessAdjustmentUntilButtonReleased)
}
}
/**
* gets the current brightness in hex format
* length - the length of the number after leading 0's have been applied. This will likely be "2"
*/
String getBrightnessHex(int length) {
StringBuilder sb = new StringBuilder(Integer.toHexString((Integer) Math.round(state.brightness * 2.55)))
if (sb.size() > length) return sb.toString()
for (int i = sb.size(); i < length; i++) {
sb.insert(0, '0')
}
return sb.toString()
}
/**
*formats a decimal value with leading 0's
*value - the value to be formatted
*length - the size of the number after leading 0's have been added
*/
String formatNumber(int value, int length) {
return String.format("%" + length + "d", value);
}
/**
*performs configuration and bindings
*/
def configure() {
if (state.boundnetwork && state.bounddimmer && state.boundonoff){
log.debug("configuration complete")
sendEvent(name:"Configured", value:"true")
return
}
log.debug "Confuguring Reporting and Bindings."
fireCommands(zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(0x0001, 0x0020))
configureDaemon()
return configCmds
}
/*
* handles binding if switch doesn't pay attention the first time.
*/
def configureDaemon(){
return runIn(8, "configure")
}
def mySleep(int ms) {
def start = now()
while (now() < start + ms) {
}
}
/**
*Refresh support. Causes battery status update and others
*/
def refresh() {
fireCommands(zigbee.readAttribute(0x0001, 0x0020) +zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.readAttribute(0x0001, 0x0020) )
}
/**
*actions to be taken once the device is installed
*/
def installed() {
log.info(device.name + " installed!!!")
sendEvent(name:"Configured", value:"false")
state.boundnetwork=false
state.bounddimmer=false
state.boundonoff=false
state.on = false
state.lastAction = 0
state.brightness = 0
state.buttonNumber = 1
state.value = "unknown"
state.lastHeld = "none"
state.battery = 100
state.dimming = false
return reportOnState(getOnState())
}
def updated(){
log.info("updated")
return refresh()
}
/**
*handles battery messages
*/
private Map batteryHandler(Map rawValue) {
int value = Integer.valueOf(rawValue.value, 16)
def linkText = getLinkText(device)
def result = [ name: 'battery', value: state.battery ]
def volts = Integer.valueOf(rawValue.value, 16) / 10
def descriptionText
if (rawValue == 0) {
state.battery = "unknown"
} else {
if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
state.battery = "overvoltage"
} else if (volts > 0) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
state.battery = Math.min(100, (int) pct * 100)
result.battery = state.battery
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
sendEvent(name: 'battery', value: state.battery, units: "%")
log.debug "${result?.descriptionText}"
return result
}
/**
* handle level commands,
* level=desired level
* duration=desired time-to-level
*/
def setLevel(Double level, Double duration) {
if (level > 100 || level < 0 || duration < 0 || duration > 9999) {
log.debug("Maximum parameters (level 0-100, duration 0-9999)-- commanded level:" + level + " commanded duration:" + duration)
return
}
log.info("Brightness commanded to " + level + "%")
state.brightness = (int) level
def result = createStCommand(" 8 4 {" + getBrightnessHex(2) + " " + formatNumber((int) duration, 4) + "}")
if (state.on != "on") on()
fireCommands(result.command) //send it to the hub for processing
reportOnState(true)
}
/**
*sets level using a 1-second duration
*level= percent
*/
def setLevel(Double level) {
setLevel(level, 1000)
}
/**
* Returns true if the switch is on. false if off.
*/
Boolean getOnState(){
return (state.on == "on")
}
/**
*poll support, forces an update of states
*/
def poll() {
sendEvent(name: 'brightness', value: state.brightness, units: "%")
reportOnState(getOnState())
}
/**
*takes a command and data, generates an "st cmd" array
*command- string representing the command and data to be send to the device
*returns the command for all devices on the switch
*/
def Map createStCommand(String command) {
List output = []
LinkedHashMap result = getStatus()
for (item in getDevices()) {
output.add("st cmd 0x" + item[0] + " 0x" + item[1] + " " + command)
}
result['command'] = (List)output.flatten()
return result
}
/**
* Handles event updates. All updates go here.
*/
def reportOnState(boolean on) {
String onValue = (on ? "on" : "off")
sendEvent(name: 'state', type:"thing", value:onValue)
sendEvent(name: 'battery', value: state.battery)
sendEvent(name: 'level', value: state.brightness)
sendEvent(name: 'button', value: state.value)
sendEvent(name: 'numberOfButtons', value: 5)
sendEvent(name: 'State Array', value: state)
sendEvent(name: 'Awesomeness Level', value: "over 9000")
state.on = onValue
}
def updateButtonState(def value) {
state.value = value
sendEvent(name: "button", value: value, unit: "")
}
/**
*commands the opposite of the current state
*if on, turn off, if off, turn on.
*/
def toggle() {
Map command
if (state.on == "on") {
log.debug(device.displayName + " toggled on")
command = off()
} else {
command = on()
log.debug(device.displayName + " toggled off")
}
log.debug(command)
fireCommands(command.command)
}
/**
*detects the current state versus commanded
*if curret is off, and commanded is off, returns true and same for inverse
*/
boolean doubleTapped(boolean commanded) {
boolean on=(state.on=="on")
if (on && commanded|| !on && !commanded) return true
return false
}
/**
*This switch does not support pressing the same button in rapid sucession
*this check finds out if user tapped one button and then the other within 1.5 seconds.
*/
boolean onOffTapped(){
return (state.lastAction < now()+1500)
}
//TODO create a up-down tapped mechanism.
/**
*turns light on.
*if already on, commands max
*/
Map on() {
log.debug(device.displayName + " commanded on" )
if (onOffTapped){
}
state.lastAction=now()
if (doubleTapped(true)) setLevel(100, 1000)
reportOnState(true)
return createStCommand("6 1 {}")
}
/**
*turns light off
*if already off, commands on to minimum
*/
Map off() {
log.debug(device.displayName + " commanded off")
if (onOffTapped){
}
state.lastAction=now()
if (doubleTapped(false)){
def value=on()
setLevel(1, 1000)
return value
}
reportOnState(false)
return createStCommand("6 0 {}")
}
/**
*Action to be taken when both buttons are pressed at the same time
*/
def bothButtonsPressed() {
log.error("Both Buttons Pressed" + state)
installed()
state.lastAction = 100
updateButtonState("Initiating configuration routines. Please stand by.")
configure()
return state
}