From Red Parakeet, 7 Years ago, written in Groovy.
Embed
  1. /**
  2. *    SmartThings Lightify Dimmer Switch support
  3. *    Copyright (C) 2016  Adam Outler
  4. *
  5. *    This program is free software: you can redistribute it and/or modify
  6. *    it under the terms of the GNU General Public License as published by
  7. *    the Free Software Foundation, either version 3 of the License, or
  8. *    (at your option) any later version.
  9. *
  10. *    This program is distributed in the hope that it will be useful,
  11. *    but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. *    GNU General Public License for more details.
  14. *
  15. *    You should have received a copy of the GNU General Public License
  16. *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19.  
  20. /**
  21. * Command reference
  22. * on  'catchall: 0104 0006 01 01 0140 00 3A68 01 00 0000 01 00 '
  23. * off  'catchall: 0104 0006 01 01 0140 00 3A68 01 00 0000 00 00 '
  24. * held up   'catchall: 0104 0008 01 01 0140 00 3A68 01 00 0000 05 00 0032'
  25. * held down  'catchall: 0104 0008 01 01 0140 00 3A68 01 00 0000 01 00 0132'
  26. * released   'catchall: 0104 0008 01 01 0140 00 3A68 01 00 0000 03 00 '
  27. */
  28.  
  29.  
  30. /**
  31. *sets up fingerprint for autojoin
  32. *sets up commands for polling and others
  33. *sets up capabilities
  34. *sets up variables
  35. */
  36. metadata {
  37.  definition(name: "Lightify Dimming Switch- Zigbee", namespace: "adamoutler", author: "Adam Amber House") {
  38.   capability "Battery"
  39.   capability "Button"
  40.   capability "Switch Level"
  41.   attribute "State Array", "string"
  42.   attribute 'Awesomeness Level', "string"
  43.   attribute 'state', "string"
  44.   command "refresh"
  45.   command "poll"
  46.   command "toggle"
  47.   command "configure"
  48.   command "installed"
  49.   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"
  50.  }
  51.  
  52.  
  53.  simulator {
  54.   // Simulations are for loosers, work in production :D
  55.  }
  56.  
  57.  
  58. preferences {
  59.   section("Device1") {
  60.    input("device1", "string", title: "Device Network ID 1", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
  61.    input("end1", "string", title: "Device Endpoint ID 1", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
  62.   }
  63.   section("Device2") {
  64.    input("device2", "string", title: "Device Network ID 2", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
  65.    input("end2", "string", title: "Device Endpoint ID 2", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
  66.   }
  67.   section("Device3") {
  68.    input("device3", "string", title: "Device Network ID 3", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
  69.    input("end3", "string", title: "Device Endpoint ID 3", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
  70.   }
  71.   section("Device4") {
  72.    input("device4", "string", title: "Device Network ID 4", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
  73.    input("end4", "string", title: "Device Endpoint ID 4", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
  74.   }
  75.   section("Device5") {
  76.    input("device5", "string", title: "Device Network ID 5", description: "The Device Network Id", defaultValue: "", type: "capability.switch", required: false, displayDuringSetup: false)
  77.    input("end5", "string", title: "Device Endpoint ID 5", description: "endpointId from Data Section of device", defaultValue: "", required: false, displayDuringSetup: false)
  78.   }
  79.  }
  80.  
  81.  
  82. /**
  83. *UI tile definitions
  84. */
  85.  tiles(scale: 2) {
  86.   standardTile("button", "device.state", width: 6, height: 4) {
  87.    state "off", label: 'Off', action: "toggle", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
  88.    state "on", label: "On", action: "toggle", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
  89.    state "turningOn", label: 'Turning on', icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "turningOff"
  90.    state "turningOff", label: 'Turning off', icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
  91.   }
  92.   valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
  93.    state "battery", label: 'battery ${currentValue}%'
  94.   }
  95.   valueTile("brightness", "device.level", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
  96.    state "brightness", label: 'brightnessn${currentValue}%'
  97.   }
  98.   standardTile("refresh", "device.button", decoration: "flat", width: 2, height: 2) {
  99.    state "default", label: "", action: "refresh", icon: "st.secondary.refresh"
  100.   }
  101.   controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 6, inactiveLabel: false, range:"(1..100)") {
  102.     state "level", action:"setLevel"
  103.   }
  104.   main "button"
  105.   details(["button", "levelSliderControl", "switch", "battery", "brightness", "refresh"])
  106.  }
  107. }
  108.  
  109. /**
  110. *returns a map representing important states
  111. */
  112. private Map getStatus() {
  113.  return [name: 'button',
  114.   brightness: state.brightness,
  115.   battery: state.battery,
  116.   value: state.value,
  117.   level: state.level,
  118.   lastAction: state.lastAction,
  119.   on: state.on,
  120.   data: '',
  121.   descriptionText: "$device.displayName button $state.buttonNumber was $value"
  122.  ]
  123. }
  124.  
  125.  
  126. /**
  127.  *Gets a list of devices in [address,endpoint] format compiled from inputs above
  128.  *returns: Arraylist<[address,endpoint]>
  129.  */
  130. final ArrayList < String[] > getDevices() {
  131.  String[] devs = [settings.device1, device2, device3, device4, device5]
  132.  if (devs == [null, null, null, null, null]) log.info("------No devices configured in $device preferences--------")
  133.  String[] ends = [end1, end2, end3, end4, end5]
  134.  ArrayList < String[] > list = new ArrayList < > ([])
  135.  for (int i = 0; i < 4; i++) {
  136.   if (devs[i] != null) {
  137.    list.add([devs[i], ends[i]])
  138.   }
  139.  }
  140.  return list
  141. }
  142.  
  143. /**
  144. *Parse events into attributes.
  145. */
  146. def parse(String msgFromST) {
  147.  log.debug(msgFromST)
  148.  if (msgFromST?.startsWith('catchall:')) {
  149.   def value = handleMessage(msgFromST)
  150.   fireCommands(value.command)
  151.  } else if (msgFromST.startsWith("read")) {
  152.   if (msgFromST.contains("attrId: 0000")) return state
  153.   if (msgFromST.contains("attrId: 0020,")) return batteryHandler(zigbee.parseDescriptionAsMap(msgFromST))
  154.   log.error('unrecognized command:' + msgFromST)
  155.  } else {
  156.   log.error('unrecognized command:' + msgFromST)
  157.  }
  158.  return getStatus()
  159. }
  160.  
  161.  
  162. /**
  163. *fire commands into the hub
  164. *commands - an array of string commands to be fired
  165. */
  166. private fireCommands(List commands) {
  167.  
  168.  
  169.   if (commands != null && commands.size() > 0) {
  170.   log.trace("Executing commands-- state:" + state + " commands:" + commands)
  171.   for (String value : commands){
  172.    sendHubCommand([value].collect {new physicalgraph.device.HubAction(it)
  173.   })
  174.   }
  175.  }
  176. }
  177.  
  178.  
  179. Map handleMessage(String msgFromST) {
  180.  Map msg = zigbee.parseDescriptionAsMap(msgFromST)
  181.  switch (Integer.parseInt(msg.clusterId)) {
  182.   case 6: //button press
  183.    def returnval = handleButtonPress(msg)
  184.    return returnval
  185.    break
  186.   case 8:
  187.    handleButtonHeld(msg)
  188.    break
  189.   case 8021:
  190.    log.info("Networking Bind Response received!!!")
  191.    updateButtonState("Network binding complete!")
  192.    state.boundnetwork=true
  193.    return state
  194.    break
  195.   case 8034:
  196.    log.info("Network managment Leave Response!!!")
  197.    state.boundnetwork=false
  198.    return state
  199.    break
  200.    log.error("Unhandled message: " + msg)
  201.    break
  202.  }
  203. }
  204.  
  205.  
  206. /**
  207. * This is the handler for button presses.  Map is routed here once a button press has been detected
  208. */
  209. def Map handleButtonPress(Map msg) {
  210.  switch (msg.command) {
  211.   case "01":
  212.    def returnval = on()
  213.    updateButtonState("on")
  214.    return returnval
  215.    break
  216.   case "03":
  217.    bothButtonsPressed()
  218.    break
  219.   case "00":
  220.    def returnval = off()
  221.    updateButtonState("off")
  222.    return returnval
  223.    break
  224.   case "07":
  225.    log.info("Button Press Bind Response!!!")
  226.    state.boundonoff=true
  227.    return state
  228.   break
  229.    log.error(getLinkText(device)+" got unknown button press command: " + msg.command)
  230.    return [name:"error",value:"unknown button press command"]
  231.    break
  232.  }
  233. }
  234.  
  235. /**
  236. *this is the handler for button held events. Map is routed here once a button held event has been detected
  237. */
  238. def Map handleButtonHeld(Map msg) {
  239.  switch (Integer.parseInt(msg.command)) {
  240.   case 1:
  241.    log.debug("Button held- Lowering brightness commanded")
  242.    state.dimming = true
  243.    updateButtonState("lowering brightness")
  244.    state.lastHeld = "down"
  245.    return adjustBrightness(false, state.brightness)
  246.    break
  247.   case 3:
  248.    log.debug("stop brightness commanded")
  249.    updateButtonState(state.lastHeld + " released")
  250.    state.dimming = false
  251.    return state
  252.    break
  253.   case 5:
  254.    log.debug("Button held- raising brightness commanded")
  255.    state.lastHeld = "up"
  256.    updateButtonState("raising brightness")
  257.    state.dimming = true
  258.    return adjustBrightness(true, state.brightness)
  259.    break
  260.   case 7:
  261.    log.info("Button Held Bind Response - 7!!!")
  262.    state.bounddimmer=true
  263.    return state
  264.   break
  265.   case 8:
  266.    log.info("Button Held Bind Response - 8!!!")
  267.    state.bounddimmer=true
  268.   return state
  269.   break
  270.    log.error("Unhandled button held event: " + msg)
  271.    break
  272.  }
  273.  return msg
  274. }
  275.  
  276.  
  277. /**
  278.  * adjusts brightness up/down depending on the value of the up boolean true is up, false is down
  279.  * continues to adjust until state.dimming is changed
  280.  * up- true if we are adjusting brightness up, false if down
  281.  * level - first level commanded
  282.  */
  283. def Map adjustBrightness(final boolean up, double level) {
  284.  Map result
  285.  log.debug("adjusting brightness" + (up?"up" : "down") + " from current " + state.brightness)
  286.  if (state.dimming) {
  287.   //increase or decrease brightness
  288.   if (up) {
  289.    state.brightening = true
  290.   } else {
  291.    state.brightening = false
  292.   }
  293.   state.brightness = (int) level
  294.   executeBrightnessAdjustmentUntilButtonReleased()
  295.  } else {
  296.   log.debug("Final brightness adjusted to " + state.brightness)
  297.  }
  298.  
  299.  sendEvent(name: "brightness", value: state.brightness)
  300.  return getStatus()
  301. }
  302.  
  303. /**
  304.  * performs a recursive brightness adjustment based on state.brightening while state.dimming is true
  305.  */
  306. void executeBrightnessAdjustmentUntilButtonReleased() {
  307.  if (state.dimming) {
  308.   if (state.brightening) {
  309.    state.brightness = state.brightness + 20
  310.   } else {
  311.    state.brightness = state.brightness - 20
  312.   }
  313.   if (state.brightness > 100) {
  314.    state.brightness = 100
  315.    state.dimming=false
  316.   }
  317.   if (state.brightness < 1){
  318.    state.brightness = 1
  319.    state.dimming=false
  320.   }
  321.   setLevel((double) state.brightness, 1000)
  322.   reportOnState(true) //Manage and report states
  323.   runIn(1, executeBrightnessAdjustmentUntilButtonReleased)
  324.  }
  325. }
  326.  
  327.  
  328. /**
  329. * gets the current brightness in hex format
  330. * length - the length of the number after leading 0's have been applied.  This will likely be "2"
  331. */
  332. String getBrightnessHex(int length) {
  333.  StringBuilder sb = new StringBuilder(Integer.toHexString((Integer) Math.round(state.brightness * 2.55)))
  334.  if (sb.size() > length) return sb.toString()
  335.  for (int i = sb.size(); i < length; i++) {
  336.   sb.insert(0, '0')
  337.  }
  338.  return sb.toString()
  339. }
  340.  
  341. /**
  342. *formats a decimal value with leading 0's
  343. *value - the value to be formatted
  344. *length - the size of the number after leading 0's have been added
  345. */
  346. String formatNumber(int value, int length) {
  347.  return String.format("%" + length + "d", value);
  348. }
  349.  
  350. /**
  351. *performs configuration and bindings
  352. */
  353. def configure() {
  354.  if (state.boundnetwork && state.bounddimmer && state.boundonoff){
  355.   log.debug("configuration complete")
  356.   sendEvent(name:"Configured", value:"true")
  357.  }
  358.  log.debug "Confuguring Reporting and Bindings."
  359.  fireCommands(zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(0x0001, 0x0020))
  360.  configureDaemon()
  361.  return configCmds
  362. }
  363.  
  364. /*
  365. * handles binding if switch doesn't pay attention the first time.
  366. */
  367. def configureDaemon(){
  368.   return runIn(8, "configure")
  369. }
  370.  
  371.  
  372.  
  373. def mySleep(int ms) {
  374.         def start = now()
  375.         while (now() < start + ms) {
  376.     }
  377. }
  378.  
  379.  
  380. /**
  381. *Refresh support.  Causes battery status update and others
  382. */
  383. def refresh() {
  384.    fireCommands(zigbee.readAttribute(0x0001, 0x0020) +zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.readAttribute(0x0001, 0x0020) )
  385. }
  386.  
  387.  
  388. /**
  389. *actions to be taken once the device is installed
  390. */
  391.  
  392. def installed() {
  393.  log.info(device.name + " installed!!!")
  394.  sendEvent(name:"Configured", value:"false")
  395.  state.boundnetwork=false
  396.  state.bounddimmer=false
  397.  state.boundonoff=false
  398.  state.on = false
  399.  state.lastAction = 0
  400.  state.brightness = 0
  401.  state.buttonNumber = 1
  402.  state.value = "unknown"
  403.  state.lastHeld = "none"
  404.  state.battery = 100
  405.  state.dimming = false
  406.  return reportOnState(getOnState())
  407. }
  408.  
  409. def updated(){
  410.  log.info("updated")
  411.  return  refresh()
  412. }
  413.  
  414.  
  415.  
  416. /**
  417. *handles battery messages
  418. */
  419. private Map batteryHandler(Map rawValue) {
  420.   int value = Integer.valueOf(rawValue.value, 16)
  421.   def linkText = getLinkText(device)
  422.   def result = [ name: 'battery', value: state.battery ]
  423.   def volts = Integer.valueOf(rawValue.value, 16) / 10
  424.   def descriptionText
  425.   if (rawValue == 0) {
  426.    state.battery = "unknown"
  427.   } else {
  428.    if (volts > 3.5) {
  429.     result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
  430.     state.battery = "overvoltage"
  431.    } else if (volts > 0) {
  432.     def minVolts = 2.1
  433.     def maxVolts = 3.0
  434.     def pct = (volts - minVolts) / (maxVolts - minVolts)
  435.     state.battery = Math.min(100, (int) pct * 100)
  436.     result.battery = state.battery
  437.     result.descriptionText = "${linkText} battery was ${result.value}%"
  438.    }
  439.   }
  440.   sendEvent(name: 'battery', value: state.battery, units: "%")
  441.   log.debug "${result?.descriptionText}"
  442.   return result
  443.  }
  444.  
  445.  
  446.  /**
  447.   * handle level commands,
  448.   * level=desired level
  449.   * duration=desired time-to-level
  450.   */
  451. def setLevel(Double level, Double duration) {
  452.  if (level > 100 || level < 0 || duration < 0 || duration > 9999) {
  453.   log.debug("Maximum parameters (level 0-100, duration 0-9999)-- commanded level:" + level + " commanded duration:" + duration)
  454.  }
  455.  log.info("Brightness commanded to " + level + "%")
  456.  state.brightness = (int) level
  457.  def result = createStCommand(" 8 4 {" + getBrightnessHex(2) + " " + formatNumber((int) duration, 4) + "}")
  458.  if (state.on != "on") on()
  459.  fireCommands(result.command) //send it to the hub for processing
  460.  reportOnState(true)
  461. }
  462.  
  463. /**
  464. *sets level using a 1-second duration
  465. *level= percent
  466. */
  467. def setLevel(Double level) {
  468.  setLevel(level, 1000)
  469. }
  470.  
  471. /**
  472. * Returns true if the switch is on.  false if off.
  473. */
  474. Boolean getOnState(){
  475.     return (state.on == "on")
  476. }
  477.  
  478. /**
  479. *poll support, forces an update of states
  480. */
  481. def poll() {
  482.  sendEvent(name: 'brightness', value: state.brightness, units: "%")
  483.  reportOnState(getOnState())
  484. }
  485.  
  486. /**
  487. *takes a command and data, generates an "st cmd" array
  488. *command- string representing the command and data to be send to the device
  489. *returns the command for all devices on the switch
  490. */
  491. def Map createStCommand(String command) {
  492.  List<String> output = []
  493.  LinkedHashMap result = getStatus()
  494.  for (item in getDevices()) {
  495.   output.add("st cmd 0x" + item[0] + " 0x" + item[1] + " " + command)
  496.  }
  497.  result['command'] = (List)output.flatten()
  498.  return result
  499. }
  500.  
  501. /**
  502.  * Handles event updates.  All updates go here.
  503.  */
  504. def reportOnState(boolean on) {
  505.  String onValue = (on ? "on" : "off")
  506.  sendEvent(name: 'state', type:"thing", value:onValue)
  507.  sendEvent(name: 'battery', value: state.battery)
  508.  sendEvent(name: 'level', value: state.brightness)
  509.  sendEvent(name: 'button', value: state.value)
  510.  sendEvent(name: 'numberOfButtons', value: 5)
  511.  sendEvent(name: 'State Array', value: state)
  512.  sendEvent(name: 'Awesomeness Level', value: "over 9000")
  513.  
  514.  state.on = onValue
  515. }
  516.  
  517. def updateButtonState(def value) {
  518.  state.value = value
  519.  sendEvent(name: "button", value: value, unit: "")
  520. }
  521.  
  522. /**
  523. *commands the opposite of the current state
  524. *if on, turn off, if off, turn on.
  525. */
  526. def toggle() {
  527.  Map command
  528.  if (state.on == "on") {
  529.   log.debug(device.displayName + " toggled on")
  530.   command = off()
  531.  } else {
  532.   command = on()
  533.   log.debug(device.displayName + " toggled off")
  534.  }
  535.  log.debug(command)
  536.  fireCommands(command.command)
  537. }
  538.  
  539. /**
  540. *detects the current state versus commanded
  541. *if curret is off, and commanded is off, returns true and same for inverse
  542. */
  543. boolean doubleTapped(boolean commanded) {
  544.  boolean on=(state.on=="on")
  545.  if (on && commanded|| !on && !commanded) return true
  546. }
  547.  
  548. /**
  549. *This switch does not support pressing the same button in rapid sucession
  550. *this check finds out if user tapped one button and then the other within 1.5 seconds.
  551. */
  552. boolean onOffTapped(){
  553.  return (state.lastAction < now()+1500)
  554. }
  555.  
  556. //TODO create a up-down tapped mechanism.
  557.  
  558.  
  559. /**
  560. *turns light on.
  561. *if already on, commands max
  562. */
  563. Map on() {
  564.  log.debug(device.displayName + " commanded on" )
  565.  if (onOffTapped){
  566.    
  567.  }
  568.  state.lastAction=now()
  569.  if (doubleTapped(true)) setLevel(100, 1000)
  570.  reportOnState(true)
  571.  return createStCommand("6 1 {}")
  572. }
  573.  
  574. /**
  575. *turns light off
  576. *if already off, commands on to minimum
  577. */
  578. Map off() {
  579.  log.debug(device.displayName + " commanded off")
  580.  if (onOffTapped){
  581.    
  582.  }
  583.  state.lastAction=now()
  584.  if (doubleTapped(false)){
  585.     def value=on()
  586.     setLevel(1, 1000)
  587.     return value
  588.  }
  589.  reportOnState(false)
  590.  return createStCommand("6 0 {}")
  591. }
  592.  
  593.  
  594. /**
  595. *Action to be taken when both buttons are pressed at the same time
  596. */
  597. def bothButtonsPressed() {
  598.  log.error("Both Buttons Pressed" + state)
  599.  installed()
  600.  state.lastAction = 100
  601.  updateButtonState("Initiating configuration routines.  Please stand by.")
  602.  configure()
  603.  return state
  604. }