├── README.md ├── examples └── drivers │ ├── virtualOpenVentArea.groovy │ ├── componentSwitch.groovy │ ├── virtualActuator.groovy │ ├── genericComponentDimmer.groovy │ ├── httpGetSwitch.groovy │ ├── sofabatonX1S.groovy │ ├── kasaPlugHubRebooter.groovy │ ├── genericComponentParentDemo.groovy │ ├── basicZWaveTool.groovy │ ├── virtualRGBW.groovy │ ├── virtualOmniSensor.groovy │ ├── neeoRemote.groovy │ ├── environmentSensor.groovy │ ├── virtualLock.groovy │ ├── virtualThermostat.groovy │ ├── thirdRealityMatterNightLight.groovy │ ├── genericZWaveCentralSceneDimmer.groovy │ ├── LifxColorBulbLegacy.groovy │ └── haloSmokeCoDetector.groovy ├── example-apps ├── switchContact.groovy ├── contactMotion.groovy ├── energySum.groovy ├── switchLock.groovy ├── averageHumidity.groovy ├── averageIlluminance.groovy ├── debounceContact.groovy ├── batteryReport.groovy ├── allOff.groovy ├── minMaxTemp.groovy ├── averageTemp.groovy ├── modeSwitches.groovy ├── lightsUsage.groovy └── autoDimmer.groovy └── smartapps └── hubitat └── Send_Hub_Events.src └── Send_Hub_Events.groovy /README.md: -------------------------------------------------------------------------------- 1 | # HubitatPublic -------------------------------------------------------------------------------- /examples/drivers/virtualOpenVentArea.groovy: -------------------------------------------------------------------------------- 1 | metadata { 2 | definition (name: "Virtual Open Vent Area", namespace: "hubitat", author: "Bruce Ravenel") { 3 | capability "Sensor" 4 | command "setOpenVentArea", ["NUMBER"] 5 | attribute "openVentArea", "Number" 6 | 7 | } 8 | preferences { 9 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 10 | } 11 | } 12 | 13 | def installed() { 14 | log.warn "installed..." 15 | setOpenVentArea(0) 16 | } 17 | 18 | def updated() { 19 | log.info "updated..." 20 | log.warn "description logging is: ${txtEnable == true}" 21 | } 22 | 23 | def parse(String description) { 24 | } 25 | 26 | def setOpenVentArea(area) { 27 | def descriptionText = "${device.displayName} was set to $area" 28 | if (txtEnable) log.info "${descriptionText}" 29 | sendEvent(name: "openVentArea", value: area, unit: "sq. m", descriptionText: descriptionText) 30 | } 31 | -------------------------------------------------------------------------------- /example-apps/switchContact.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Switch-Contact", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Turn a Switch into a Contact Sensor", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | def mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this Switch-Contact", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") 19 | input "switches", "capability.switch", title: "Select Switches", submitOnChange: true, required: true, multiple: true 20 | } 21 | } 22 | } 23 | 24 | def installed() { 25 | initialize() 26 | } 27 | 28 | def updated() { 29 | unsubscribe() 30 | initialize() 31 | } 32 | 33 | def initialize() { 34 | def contactDev = getChildDevice("SwitchContact_${app.id}") 35 | if(!contactDev) contactDev = addChildDevice("hubitat", "Virtual Contact Sensor", "SwitchContact_${app.id}", null, [label: thisName, name: thisName]) 36 | subscribe(switches, "switch", handler) 37 | } 38 | 39 | def handler(evt) { 40 | def contactDev = getChildDevice("SwitchContact_${app.id}") 41 | if(evt.value == "on") contactDev.open() else contactDev.close() 42 | log.info "Switch $evt.device $evt.value" 43 | } 44 | -------------------------------------------------------------------------------- /example-apps/contactMotion.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Contact-Motion", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Turn Contact Sensor into Motion Sensor", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | def mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this Contact-Motion", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") 19 | input "contactSensors", "capability.contactSensor", title: "Select Contact Sensors", submitOnChange: true, required: true, multiple: true 20 | } 21 | } 22 | } 23 | 24 | def installed() { 25 | initialize() 26 | } 27 | 28 | def updated() { 29 | unsubscribe() 30 | initialize() 31 | } 32 | 33 | def initialize() { 34 | def motionDev = getChildDevice("ContactMotion_${app.id}") 35 | if(!motionDev) motionDev = addChildDevice("hubitat", "Virtual Motion Sensor", "ContactMotion_${app.id}", null, [label: thisName, name: thisName]) 36 | subscribe(contactSensors, "contact", handler) 37 | } 38 | 39 | def handler(evt) { 40 | def motionDev = getChildDevice("ContactMotion_${app.id}") 41 | if(evt.value == "open") motionDev.active() else motionDev.inactive() 42 | log.info "Contact $evt.device $evt.value" 43 | } 44 | -------------------------------------------------------------------------------- /examples/drivers/componentSwitch.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Generic Component Switch 3 | Copyright 2016 -> 2020 Hubitat Inc. All Rights Reserved 4 | 2020-04-16 2.2.0 maxwell 5 | -refactor 6 | 2018-12-15 maxwell 7 | -initial pub 8 | 9 | */ 10 | 11 | metadata { 12 | definition(name: "Generic Component Switch", namespace: "hubitat", author: "mike maxwell", component: true) { 13 | capability "Switch" 14 | capability "Refresh" 15 | capability "Actuator" 16 | } 17 | preferences { 18 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 19 | } 20 | } 21 | 22 | void updated() { 23 | log.info "Updated..." 24 | log.warn "description logging is: ${txtEnable == true}" 25 | } 26 | 27 | void installed() { 28 | log.info "Installed..." 29 | device.updateSetting("txtEnable",[type:"bool",value:true]) 30 | refresh() 31 | } 32 | 33 | void parse(String description) { log.warn "parse(String description) not implemented" } 34 | 35 | void parse(List description) { 36 | description.each { 37 | if (it.name in ["switch"]) { 38 | if (txtEnable) log.info it.descriptionText 39 | sendEvent(it) 40 | } 41 | } 42 | } 43 | 44 | void on() { 45 | parent?.componentOn(this.device) 46 | } 47 | 48 | void off() { 49 | parent?.componentOff(this.device) 50 | } 51 | 52 | void refresh() { 53 | parent?.componentRefresh(this.device) 54 | } 55 | -------------------------------------------------------------------------------- /examples/drivers/virtualActuator.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Virtual Actuator 3 | 4 | Copyright 2016-2021 Hubitat Inc. All Rights Reserved 5 | 6 | 7 | */ 8 | 9 | metadata { 10 | definition (name: "Virtual Actuator", namespace: "hubitat", author: "Bruce Ravenel") { 11 | capability "Actuator" 12 | command "on" 13 | command "off" 14 | command "neutral" 15 | attribute "switchPosition", "ENUM" 16 | } 17 | preferences { 18 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 19 | } 20 | } 21 | 22 | def installed() { 23 | log.warn "installed..." 24 | off() 25 | select("off") 26 | } 27 | 28 | def updated() { 29 | log.warn "updated..." 30 | log.warn "description logging is: ${txtEnable == true}" 31 | } 32 | 33 | def parse(String description) { 34 | } 35 | 36 | def on() { 37 | def descriptionText = "${device.displayName} was turned on" 38 | if (txtEnable) log.info "${descriptionText}" 39 | sendEvent(name: "switchPosition", value: "on", descriptionText: descriptionText) 40 | } 41 | 42 | def off() { 43 | def descriptionText = "${device.displayName} was turned off" 44 | if (txtEnable) log.info "${descriptionText}" 45 | sendEvent(name: "switchPosition", value: "off", descriptionText: descriptionText) 46 | } 47 | 48 | def neutral() { 49 | def descriptionText = "${device.displayName} was turned neutral" 50 | if (txtEnable) log.info "${descriptionText}" 51 | sendEvent(name: "switchPosition", value: "neutral", descriptionText: descriptionText) 52 | } 53 | -------------------------------------------------------------------------------- /example-apps/energySum.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Energy Sum", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Add up some energy reports", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | def mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this energy sum", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") 19 | input "energymeters", "capability.energyMeter", title: "Select Energy Meters", submitOnChange: true, required: true, multiple: true 20 | if(energymeters) paragraph "Current sum is ${sumEnergy()}" 21 | } 22 | } 23 | } 24 | 25 | def installed() { 26 | initialize() 27 | } 28 | 29 | def updated() { 30 | unsubscribe() 31 | initialize() 32 | } 33 | 34 | def initialize() { 35 | def energySum = getChildDevice("EnergySum_${app.id}") 36 | if(!energySum) energySum = addChildDevice("hubitat", "Virtual Omni Sensor", "EnergySum_${app.id}", null, [label: thisName, name: thisName]) 37 | energySum.setEnergy(sumEnergy()) 38 | subscribe(energymeters, "energy", handler) 39 | } 40 | 41 | def sumEnergy() { 42 | def total = 0 43 | energymeters.each {total += it.currentEnergy} 44 | return total 45 | } 46 | 47 | def handler(evt) { 48 | def energySum = getChildDevice("EnergySum_${app.id}") 49 | def sum = sumEnergy() 50 | energySum.setEnergy(sum) 51 | log.info "Energy sum = $sum" 52 | } 53 | -------------------------------------------------------------------------------- /example-apps/switchLock.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Switch-Lock", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Use Switch to Lock/Unlock", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | Map mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this Switch-Lock", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") 19 | input "lock", "capability.lock", title: "Select Lock", submitOnChange: true, required: true 20 | input "lockOnly", "bool", title: "Lock only?", submitOnChange: true, width: 3 21 | input "unlockOnly", "bool", title: "Unock only?", submitOnChange: true, width: 3 22 | } 23 | } 24 | } 25 | 26 | def installed() { 27 | initialize() 28 | } 29 | 30 | def updated() { 31 | unsubscribe() 32 | initialize() 33 | } 34 | 35 | void initialize() { 36 | def switchDev = getChildDevice("SwitchLock_${app.id}") 37 | if(!switchDev) switchDev = addChildDevice("hubitat", "Room Lights Activator Switch", "SwitchLock_${app.id}", null, [label: lock.label, name: lock.name]) 38 | subscribe(switchDev, "switch", handler) 39 | } 40 | 41 | void handler(evt) { 42 | def switchDev = getChildDevice("SwitchLock_${app.id}") 43 | if(evt.value == "on") if(!unlockOnly) {lock.lock(); log.info "$lock locked"} 44 | else if(!lockOnly) {lock.unlock(); log.info "$lock unlocked"} 45 | if(lockOnly) switchDev.soff() 46 | if(unlockOnly) switchDev.son() 47 | } 48 | 49 | void on() {} 50 | void off() {} 51 | -------------------------------------------------------------------------------- /example-apps/averageHumidity.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Average Humidity", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Average some humidity sensors", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | def mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this humidity averager", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") 19 | input "humidSensors", "capability.relativeHumidityMeasurement", title: "Select Humidity Sensors", submitOnChange: true, required: true, multiple: true 20 | if(humidSensors) paragraph "Current average is ${averageHumid()}%" 21 | } 22 | } 23 | } 24 | 25 | def installed() { 26 | initialize() 27 | } 28 | 29 | def updated() { 30 | unsubscribe() 31 | initialize() 32 | } 33 | 34 | def initialize() { 35 | def averageDev = getChildDevice("AverageHumid_${app.id}") 36 | if(!averageDev) averageDev = addChildDevice("hubitat", "Virtual Humidity Sensor", "AverageHumid_${app.id}", null, [label: thisName, name: thisName]) 37 | averageDev.setHumidity(averageHumid()) 38 | subscribe(humidSensors, "humidity", handler) 39 | } 40 | 41 | def averageHumid() { 42 | def total = 0 43 | def n = humidSensors.size() 44 | humidSensors.each {total += it.currentHumidity} 45 | return (total / n).toDouble().round(1) 46 | } 47 | 48 | def handler(evt) { 49 | def averageDev = getChildDevice("AverageHumid_${app.id}") 50 | def avg = averageHumid() 51 | averageDev.setHumidity(avg) 52 | log.info "Average humidity = $avg%" 53 | } 54 | -------------------------------------------------------------------------------- /example-apps/averageIlluminance.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Average Illuminance", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Average some illuminance sensors", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | def mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this illuminance averager", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") 19 | input "luxSensors", "capability.illuminanceMeasurement", title: "Select Illuminance Sensors", submitOnChange: true, required: true, multiple: true 20 | if(luxSensors) paragraph "Current average is ${averageLux()} lux" 21 | } 22 | } 23 | } 24 | 25 | def installed() { 26 | initialize() 27 | } 28 | 29 | def updated() { 30 | unsubscribe() 31 | initialize() 32 | } 33 | 34 | def initialize() { 35 | def averageDev = getChildDevice("AverageLux_${app.id}") 36 | if(!averageDev) averageDev = addChildDevice("hubitat", "Virtual Illuminance Sensor", "AverageLux_${app.id}", null, [label: thisName, name: thisName]) 37 | averageDev.setLux(averageLux()) 38 | subscribe(luxSensors, "illuminance", handler) 39 | } 40 | 41 | def averageLux() { 42 | def total = 0 43 | def n = luxSensors.size() 44 | luxSensors.each {total += it.currentIlluminance} 45 | return (total / n).toDouble().round(0).toInteger() 46 | } 47 | 48 | def handler(evt) { 49 | def averageDev = getChildDevice("AverageLux_${app.id}") 50 | def avg = averageLux() 51 | averageDev.setLux(avg) 52 | log.info "Average illuminance = $avg lux" 53 | } 54 | -------------------------------------------------------------------------------- /example-apps/debounceContact.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Debounce contact", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Debounce double reporting contact sensor", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | def mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this debouncer; debounced contact device will have this name", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") else app.updateSetting("thisName", "Debounce contact") 19 | input "contact", "capability.contactSensor", title: "Select Contact Sensor", submitOnChange: true, required: true 20 | input "delayTime", "number", title: "Enter number of milliseconds to delay for debounce", submitOnChange: true, defaultValue: 1000 21 | } 22 | } 23 | } 24 | 25 | def installed() { 26 | initialize() 27 | } 28 | 29 | def updated() { 30 | unsubscribe() 31 | unschedule() 32 | initialize() 33 | } 34 | 35 | def initialize() { 36 | def debounceDev = getChildDevice("debounceSwitch_${app.id}") 37 | if(!debounceDev) debounceDev = addChildDevice("hubitat", "Virtual Contact Sensor", "debounceSwitch_${app.id}", null, [label: thisName, name: thisName]) 38 | subscribe(contact, "contact", handler) 39 | } 40 | 41 | def handler(evt) { 42 | runInMillis(delayTime, debounced, [data: [o: evt.value]]) 43 | log.info "Contact $evt.device $evt.value, start delay of $delayTime milliseconds" 44 | } 45 | 46 | def debounced(data) { 47 | log.info "Debounced contact $data.o" 48 | def debounceDev = getChildDevice("debounceSwitch_${app.id}") 49 | if(data.o == "open") debounceDev.open() else debounceDev.close() 50 | } 51 | 52 | -------------------------------------------------------------------------------- /example-apps/batteryReport.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Battery Notifier", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Battery Notifier", 6 | installOnOpen: true, 7 | category: "Convenience", 8 | iconUrl: "", 9 | iconX2Url: "" 10 | ) 11 | 12 | preferences { 13 | page(name: "mainPage") 14 | } 15 | 16 | def mainPage() { 17 | dynamicPage(name: "mainPage", title: "Battery Notifier", uninstall: true, install: true) { 18 | section { 19 | input "devs", "capability.*", title: "Select devices", submitOnChange: true, multiple: true 20 | input "notice", "capability.notification", title: "Select notification device", submitOnChange: true 21 | input "check", "button", title: "Check Now", state: "check" 22 | if(state.check) { 23 | paragraph handler() 24 | state.check = false 25 | } 26 | } 27 | } 28 | } 29 | 30 | def updated() { 31 | unschedule() 32 | unsubscribe() 33 | initialize() 34 | } 35 | 36 | def installed() { 37 | initialize() 38 | } 39 | 40 | void initialize() { 41 | schedule("0 30 9 ? * * *", handlerX) 42 | } 43 | 44 | String handler(note=false) { 45 | String s = "" 46 | def rightNow = new Date() 47 | devs.each { 48 | def lastTime = it.events(max: 1).date 49 | if(lastTime) { 50 | def minutes = ((rightNow.time - lastTime.time) / 60000).toInteger() 51 | if(minutes < 0) minutes += 1440 52 | if(minutes > 1440) s += (note ? "" : "") + "$it.displayName, " 53 | 54 | } else s += (note ? "" : "") + "$it.displayName, " + (note ? "" : "") 55 | } 56 | if(note) notice.deviceNotification(s ? "[H]${s[0..-3]} did not report" : "All devices reported") 57 | else return s ? "${s[0..-3]} did not report" : "All devices reported" 58 | } 59 | 60 | void handlerX() { 61 | handler(true) 62 | } 63 | -------------------------------------------------------------------------------- /examples/drivers/genericComponentDimmer.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Generic Component Dimmer 3 | Copyright 2016 -> 2020 Hubitat Inc. All Rights Reserved 4 | 2020-04-16 2.2.0 maxwell 5 | -add missing log method 6 | 2019-09-07 2.1.5 maxwell 7 | -refactor declarations 8 | 2018-12-15 maxwell 9 | -initial pub 10 | 11 | */ 12 | metadata { 13 | definition(name: "Generic Component Dimmer", namespace: "hubitat", author: "mike maxwell", component: true) { 14 | capability "Light" 15 | capability "Switch" 16 | capability "Switch Level" 17 | capability "ChangeLevel" 18 | capability "Refresh" 19 | capability "Actuator" 20 | } 21 | preferences { 22 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 23 | } 24 | } 25 | 26 | void updated() { 27 | log.info "Updated..." 28 | log.warn "description logging is: ${txtEnable == true}" 29 | } 30 | 31 | void installed() { 32 | log.info "Installed..." 33 | device.updateSetting("txtEnable",[type:"bool",value:true]) 34 | refresh() 35 | } 36 | 37 | void parse(String description) { log.warn "parse(String description) not implemented" } 38 | 39 | void parse(List description) { 40 | description.each { 41 | if (it.name in ["switch","level"]) { 42 | if (txtEnable) log.info it.descriptionText 43 | sendEvent(it) 44 | } 45 | } 46 | } 47 | 48 | void on() { 49 | parent?.componentOn(this.device) 50 | } 51 | 52 | void off() { 53 | parent?.componentOff(this.device) 54 | } 55 | 56 | void setLevel(level) { 57 | parent?.componentSetLevel(this.device,level) 58 | } 59 | 60 | void setLevel(level, ramp) { 61 | parent?.componentSetLevel(this.device,level,ramp) 62 | } 63 | 64 | void startLevelChange(direction) { 65 | parent?.componentStartLevelChange(this.device,direction) 66 | } 67 | 68 | void stopLevelChange() { 69 | parent?.componentStopLevelChange(this.device) 70 | } 71 | 72 | void refresh() { 73 | parent?.componentRefresh(this.device) 74 | } 75 | -------------------------------------------------------------------------------- /examples/drivers/httpGetSwitch.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Http GET Switch 3 | * 4 | * Calls URIs with HTTP GET for switch on or off 5 | * 6 | */ 7 | metadata { 8 | definition(name: "Http GET Switch", namespace: "community", author: "Community", importUrl: "https://raw.githubusercontent.com/hubitat/HubitatPublic/master/examples/drivers/httpGetSwitch.groovy") { 9 | capability "Actuator" 10 | capability "Switch" 11 | capability "Sensor" 12 | } 13 | } 14 | 15 | preferences { 16 | 17 | input "onURI", "text", title: "On URI", required: false 18 | input "offURI", "text", title: "Off URI", required: false 19 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true 20 | 21 | } 22 | 23 | def logsOff() { 24 | log.warn "debug logging disabled..." 25 | device.updateSetting("logEnable", [value: "false", type: "bool"]) 26 | } 27 | 28 | def updated() { 29 | log.info "updated..." 30 | log.warn "debug logging is: ${logEnable == true}" 31 | if (logEnable) runIn(1800, logsOff) 32 | } 33 | 34 | def parse(String description) { 35 | if (logEnable) log.debug(description) 36 | } 37 | 38 | def on() { 39 | if (logEnable) log.debug "Sending on GET request to [${settings.onURI}]" 40 | 41 | try { 42 | httpGet(settings.onURI) { resp -> 43 | if (resp.success) { 44 | sendEvent(name: "switch", value: "on", isStateChange: true) 45 | } 46 | if (logEnable) 47 | if (resp.data) log.debug "${resp.data}" 48 | } 49 | } catch (Exception e) { 50 | log.warn "Call to on failed: ${e.message}" 51 | } 52 | } 53 | 54 | def off() { 55 | if (logEnable) log.debug "Sending off GET request to [${settings.offURI}]" 56 | 57 | try { 58 | httpGet(settings.offURI) { resp -> 59 | if (resp.success) { 60 | sendEvent(name: "switch", value: "off", isStateChange: true) 61 | } 62 | if (logEnable) 63 | if (resp.data) log.debug "${resp.data}" 64 | } 65 | } catch (Exception e) { 66 | log.warn "Call to off failed: ${e.message}" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example-apps/allOff.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "All Off", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Turn Devices Off with Recheck", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "" 9 | ) 10 | 11 | preferences { 12 | page(name: "mainPage") 13 | } 14 | 15 | Map mainPage() { 16 | dynamicPage(name: "mainPage", title: "All Off", uninstall: true, install: true) { 17 | section { 18 | input "appName", "text", title: "Name this instance of All Off", submitOnChange: true 19 | if(appName) app.updateLabel(appName) 20 | input "switches", "capability.switch", title: "Switches to turn off", multiple: true 21 | paragraph "For the trigger use a Virtual Switch with auto-off enabled, turning it on fires the main off command for the switches above" 22 | input "trigger", "capability.switch", title: "Trigger switch" 23 | input "retry", "number", title: "Select retry interval in seconds (default 1 second)", defaultValue: 1, submitOnChange: true, width: 4 24 | input "maxRetry", "number", title: "Maximum number of retries?", defaultValue: 5, submitOnChange: true, width: 4 25 | input "meter", "number", title: "Use metering (in milliseconds)", width: 4 26 | } 27 | } 28 | } 29 | 30 | void updated() { 31 | unsubscribe() 32 | initialize() 33 | } 34 | 35 | void installed() { 36 | initialize() 37 | } 38 | 39 | void initialize() { 40 | subscribe(trigger, "switch.off", handler) 41 | subscribe(switches, "switch.on", onHandler) 42 | atomicState.someOn = true 43 | } 44 | 45 | void handler(evt) { 46 | atomicState.retry = 0 47 | turnOff() 48 | } 49 | 50 | void turnOff() { 51 | if(atomicState.someOn) { 52 | Boolean maybeOn = false 53 | List whichOff = [] 54 | switches.each{ 55 | if(it.currentSwitch == "on") { 56 | it.off() 57 | maybeOn = true 58 | whichOff += it 59 | if(meter) pause(meter) 60 | } 61 | } 62 | atomicState.someOn = maybeOn 63 | if(maybeOn) { 64 | log.info "Switches sent off commands: ${"$whichOff" - "[" - "]"}" 65 | atomicState.retry++ 66 | if(atomicState.retry < maxRetry) runIn(retry, turnOff) 67 | else log.info "Stopped after $maxRetry attempts: ${"$whichOff" - "[" - "]"} still on" 68 | } else log.info "All switches reported off" 69 | } 70 | } 71 | 72 | void onHandler(evt) { 73 | atomicState.someOn = true 74 | } 75 | -------------------------------------------------------------------------------- /example-apps/minMaxTemp.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Min/Max Temperatures", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Return min/max of temperature sensors", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | def mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this min/max temperature setter", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") 19 | input "tempSensors", "capability.temperatureMeasurement", title: "Select Temperature Sensors", submitOnChange: true, required: true, multiple: true 20 | if(tempSensors) { 21 | tempRes = minTemp() 22 | paragraph "Current minimum sensor is $tempRes.temp° on $tempRes.dev" 23 | Map tempRes = maxTemp() 24 | paragraph "Current maximum sensor is $tempRes.temp° on $tempRes.dev" 25 | } 26 | } 27 | } 28 | } 29 | 30 | def installed() { 31 | initialize() 32 | } 33 | 34 | def updated() { 35 | unsubscribe() 36 | initialize() 37 | } 38 | 39 | def initialize() { 40 | def minDev = getChildDevice("MinTemp_${app.id}") 41 | if(!minDev) minDev = addChildDevice("hubitat", "Virtual Temperature Sensor", "MinTemp_${app.id}", null, [label: "${thisName}-Min", name: "${thisName}-Min"]) 42 | def maxDev = getChildDevice("MaxTemp_${app.id}") 43 | if(!maxDev) maxDev = addChildDevice("hubitat", "Virtual Temperature Sensor", "MaxTemp_${app.id}", null, [label: "${thisName}-Max", name: "${thisName}-Max"]) 44 | Map tempRes = minTemp() 45 | minDev.setTemperature(tempRes.temp) 46 | log.info "Current minimum sensor is $tempRes.temp° on $tempRes.dev" 47 | tempRes = maxTemp() 48 | maxDev.setTemperature(tempRes.temp) 49 | log.info "Current maximum sensor is $tempRes.temp° on $tempRes.dev" 50 | subscribe(tempSensors, "temperature", handler) 51 | } 52 | 53 | Map maxTemp() { 54 | Map result = [temp: -1000, dev: ""] 55 | tempSensors.each{ 56 | if(it.currentTemperature > result.temp) { 57 | result.temp = it.currentTemperature 58 | result.dev = it.displayName 59 | } 60 | } 61 | return result 62 | } 63 | 64 | Map minTemp() { 65 | Map result = [temp: 1000, dev: ""] 66 | tempSensors.each{ 67 | if(it.currentTemperature < result.temp) { 68 | result.temp = it.currentTemperature 69 | result.dev = it.displayName 70 | } 71 | } 72 | return result 73 | } 74 | 75 | def handler(evt) { 76 | def minDev = getChildDevice("MinTemp_${app.id}") 77 | def maxDev = getChildDevice("MaxTemp_${app.id}") 78 | Map res = minTemp() 79 | minDev.setTemperature(res.temp) 80 | log.info "Current minimum sensor is $res.temp° on $res.dev" 81 | res = maxTemp() 82 | maxDev.setTemperature(res.temp) 83 | log.info "Current maximum sensor is $res.temp° on $res.dev" 84 | } 85 | -------------------------------------------------------------------------------- /example-apps/averageTemp.groovy: -------------------------------------------------------------------------------- 1 | definition( 2 | name: "Average Temperatures", 3 | namespace: "hubitat", 4 | author: "Bruce Ravenel", 5 | description: "Average some temperature sensors", 6 | category: "Convenience", 7 | iconUrl: "", 8 | iconX2Url: "") 9 | 10 | preferences { 11 | page(name: "mainPage") 12 | } 13 | 14 | def mainPage() { 15 | dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) { 16 | section { 17 | input "thisName", "text", title: "Name this temperature averager", submitOnChange: true 18 | if(thisName) app.updateLabel("$thisName") 19 | input "tempSensors", "capability.temperatureMeasurement", title: "Select Temperature Sensors", submitOnChange: true, required: true, multiple: true 20 | paragraph "Enter weight factors and offsets" 21 | tempSensors.each { 22 | input "weight$it.id", "decimal", title: "$it ($it.currentTemperature)", defaultValue: 1.0, submitOnChange: true, width: 3 23 | input "offset$it.id", "decimal", title: "$it Offset", defaultValue: 0.0, submitOnChange: true, range: "*..*", width: 3 24 | } 25 | input "useRun", "number", title: "Compute running average over this many sensor events:", defaultValue: 1, submitOnChange: true 26 | if(tempSensors) paragraph "Current sensor average is ${averageTemp()}°" 27 | if(useRun > 1) { 28 | initRun() 29 | if(tempSensors) paragraph "Current running average is ${averageTemp(useRun)}°" 30 | } 31 | } 32 | } 33 | } 34 | 35 | def installed() { 36 | initialize() 37 | } 38 | 39 | def updated() { 40 | unsubscribe() 41 | initialize() 42 | } 43 | 44 | def initialize() { 45 | def averageDev = getChildDevice("AverageTemp_${app.id}") 46 | if(!averageDev) averageDev = addChildDevice("hubitat", "Virtual Temperature Sensor", "AverageTemp_${app.id}", null, [label: thisName, name: thisName]) 47 | averageDev.setTemperature(averageTemp()) 48 | subscribe(tempSensors, "temperature", handler) 49 | } 50 | 51 | def initRun() { 52 | def temp = averageTemp() 53 | if(!state.run) { 54 | state.run = [] 55 | for(int i = 0; i < useRun; i++) state.run += temp 56 | } 57 | } 58 | 59 | def averageTemp(run = 1) { 60 | def total = 0 61 | def n = 0 62 | tempSensors.each { 63 | def offset = settings["offset$it.id"] != null ? settings["offset$it.id"] : 0 64 | total += (it.currentTemperature + offset) * (settings["weight$it.id"] != null ? settings["weight$it.id"] : 1) 65 | n += settings["weight$it.id"] != null ? settings["weight$it.id"] : 1 66 | } 67 | def result = total / (n = 0 ? tempSensors.size() : n) 68 | if(run > 1) { 69 | total = 0 70 | state.run.each {total += it} 71 | result = total / run 72 | } 73 | return result.toDouble().round(1) 74 | } 75 | 76 | def handler(evt) { 77 | def averageDev = getChildDevice("AverageTemp_${app.id}") 78 | def avg = averageTemp() 79 | if(useRun > 1) { 80 | state.run = state.run.drop(1) + avg 81 | avg = averageTemp(useRun) 82 | } 83 | averageDev.setTemperature(avg) 84 | log.info "Average sensor temperature = ${averageTemp()}°" + (useRun > 1 ? " Running average is $avg°" : "") 85 | } 86 | -------------------------------------------------------------------------------- /examples/drivers/sofabatonX1S.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Sofabaton X1S 3 | Copyright 2025 Hubitat Inc. All Rights Reserved 4 | 5 | 2025-03-22 maxwell 6 | -initial publication in github repo 7 | 8 | *simple example driver for Sofabaton X1S remote, allows mapping X1S remote buttons to Hubitat button events 9 | 10 | *driver configuration 11 | -set a static DHCP reservation for the XS1 hub 12 | -use that reserved IP in this drivers preference setting 13 | 14 | 15 | *mobile app configuration on the X1S side for this specific driver instance: 16 | -click add devices in devices tab, select Wi-Fi 17 | -click link at bottom "Create a virtual device for IP control" 18 | -enter http://my hubs IP address:39501/route (the route isn't actually used in this example, so the name could be anything, but it is required else the command won't be sent) 19 | -set PUT as the request method, application/json as the content type 20 | -in the body type the command text (no quotes), that you want to parse, toggle in this example code 21 | 22 | */ 23 | 24 | metadata { 25 | definition (name: "Sofabaton X1S", namespace: "hubitat", author: "Mike Maxwell") { 26 | capability "Actuator" 27 | capability "PushableButton" 28 | preferences { 29 | input name:"ip", type:"text", title: "X1S IP" 30 | input name:"logEnable", type: "bool", title: "Enable debug logging", defaultValue: false 31 | input name:"txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 32 | } 33 | } 34 | } 35 | 36 | void logsOff(){ 37 | log.warn "debug logging disabled..." 38 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 39 | } 40 | 41 | void updated(){ 42 | log.info "updated..." 43 | log.warn "debug logging is: ${logEnable == true}" 44 | log.warn "description logging is: ${txtEnable == true}" 45 | if (logEnable) runIn(1800,logsOff) 46 | if (ip) { 47 | device.deviceNetworkId = ipToHex(IP) 48 | //change button count to suit your needs 49 | sendEvent(name:"numberOfButtons",value:10) 50 | } 51 | } 52 | 53 | void parse(String description) { 54 | Map msg = parseLanMessage(description) 55 | switch (msg.body) { 56 | //add other case commands below 57 | case "toggle" : 58 | sendButtonEvent("pushed", 1, "physical") 59 | break 60 | //case "yada" : 61 | //sendButtonEvent("pushed", 2, "physical") 62 | //break 63 | default : 64 | log.debug "unknown body:${msg.body}" 65 | } 66 | } 67 | 68 | void sendButtonEvent(String evt, Integer bid, String type) { 69 | String descriptionText = "${device.displayName} button ${bid} was ${evt} [${type}]" 70 | if (txtEnable) log.info "${descriptionText}" 71 | sendEvent(name: evt, value: bid, descriptionText: descriptionText, isStateChange: true, type: type) 72 | } 73 | 74 | void push(button) { 75 | sendButtonEvent("pushed", button, "digital", "driver UI") 76 | } 77 | 78 | String ipToHex(IP) { 79 | List quad = ip.split(/\./) 80 | String hexIP = "" 81 | quad.each { 82 | hexIP+= Integer.toHexString(it.toInteger()).padLeft(2,"0").toUpperCase() 83 | } 84 | return hexIP 85 | } 86 | -------------------------------------------------------------------------------- /example-apps/modeSwitches.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Mode Switches 3 | * 4 | * Copyright 2023 Hubitat, Inc. All Rights Reserved. 5 | * 6 | */ 7 | 8 | definition( 9 | name: "Mode Switches", 10 | namespace: "hubitat", 11 | author: "Bruce Ravenel", 12 | description: "Control Switches by Mode", 13 | category: "Convenience", 14 | iconUrl: "", 15 | iconX2Url: "" 16 | ) 17 | 18 | preferences { 19 | page(name: "mainPage") 20 | } 21 | 22 | def mainPage() { 23 | if(!state.modeSwitch) state.modeSwitch = [:] 24 | dynamicPage(name: "mainPage", title: "Mode Switches", uninstall: true, install: true) { 25 | section { 26 | input "lights", "capability.switch", title: "Select Switches to Control", multiple: true, submitOnChange: true, width: 4 27 | if(lights) { 28 | lights.each{dev -> 29 | if(!state.modeSwitch[dev.id]) state.modeSwitch[dev.id] = [:] 30 | location.modes.each{if(!state.modeSwitch[dev.id]["$it.id"]) state.modeSwitch[dev.id]["$it.id"] = " "} 31 | } 32 | paragraph displayTable() 33 | input "logging", "bool", title: "Enable Logging?", defaultValue: true, submitOnChange: true 34 | } 35 | } 36 | } 37 | } 38 | 39 | String displayTable() { 40 | String str = "" 41 | str += "
" + 43 | "" 44 | location.modes.sort{it.name}.each{str += "" 46 | location.modes.each{str += ""} 47 | str += "" 48 | String X = "" 49 | String O = "" 50 | lights.sort{it.displayName.toLowerCase()}.each {dev -> 51 | String devLink = "$dev($dev.currentSwitch)" 52 | str += "" 53 | location.modes.sort{it.name}.each{ 54 | str += "" 55 | str += "" 56 | } 57 | } 58 | str += "
Modes${location.currentMode.id == it.id ? "$it.name"} 45 | str += "
SwitchesOnOff
$devLink${buttonLink("$dev.id:$it.id:on", state.modeSwitch[dev.id]["$it.id"] == "on" ? X : O, "#1A77C9")}${buttonLink("$dev.id:$it.id:off", state.modeSwitch[dev.id]["$it.id"] == "off" ? X : O, "#1A77C9")}
" 59 | str 60 | } 61 | 62 | String buttonLink(String btnName, String linkText, color = "#1A77C9", font = "15px") { 63 | "
$linkText
" 64 | } 65 | 66 | void appButtonHandler(btn) { 67 | List b = btn.tokenize(":") 68 | String s = state.modeSwitch[b[0]][b[1]] 69 | state.modeSwitch[b[0]][b[1]] = s == " " || s != b[2] ? b[2] : " " 70 | } 71 | 72 | def updated() { 73 | unsubscribe() 74 | initialize() 75 | } 76 | 77 | def installed() { 78 | initialize() 79 | } 80 | 81 | void initialize() { 82 | subscribe(location, "mode", modeHandler) 83 | } 84 | 85 | void modeHandler(evt) { 86 | if(logging) log.info "Mode is now $evt.value" 87 | lights.each{dev -> 88 | String s = state.modeSwitch[dev.id]["$location.currentMode.id"] 89 | if(s != " ") { 90 | dev."$s"() 91 | if(logging) log.info "$dev turned $s" 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /examples/drivers/kasaPlugHubRebooter.groovy: -------------------------------------------------------------------------------- 1 | metadata { 2 | definition ( 3 | name: "Kasa Plug Hub Rebooter", 4 | namespace: "hubitat", 5 | author: "Victor U." 6 | ) { 7 | command "schedulePowerCycle" 8 | } 9 | 10 | preferences { 11 | input ("device_IP", "text", 12 | title: "Device IP", 13 | defaultValue: getDataValue("deviceIP")) 14 | input ("debug", "bool", 15 | title: "Enable debug logging", 16 | defaultValue: false) 17 | input ("shutdownHub", "bool", 18 | title: "Shut hub down (leave off for testing)", 19 | defaultValue: false) 20 | } 21 | } 22 | 23 | def installed() { 24 | runIn(2, updated) 25 | } 26 | 27 | def updated() { 28 | if (debug) log.debug("updating preferences....") 29 | 30 | unschedule() 31 | interfaces.rawSocket.disconnect() 32 | 33 | if (!device_IP) { 34 | log.warn("Device IP is not set.") 35 | return 36 | } else { 37 | updateDataValue("deviceIP", device_IP.trim()) 38 | } 39 | } 40 | 41 | void schedulePowerCycle() { 42 | if (!getDataValue("deviceIP")) { 43 | log.error("No Kasa plug IP specified") 44 | return 45 | } 46 | 47 | // TP-Link commands: https://github.com/softScheck/tplink-smartplug/blob/master/tplink-smarthome-commands.txt 48 | // JavaScript API docs: https://plasticrake.github.io/tplink-smarthome-api/ 49 | log.info("scheduling a power cycle...") 50 | 51 | unschedule() 52 | resetPlugSchedule() 53 | pauseExecution(500) 54 | 55 | // calculate current minute of the day - that's what Kasa plugs schedule deals in 56 | Calendar calendar = new GregorianCalendar() 57 | int minuteOfDay = calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE) 58 | log.info "scheduling plug to turn off at ${minutesToReadable(minuteOfDay+2)} and turn on at ${minutesToReadable(minuteOfDay+3)}..." 59 | 60 | // schedule command to power DOWN 61 | sendCmd(outputXOR("""{"schedule":{"add_rule":{"stime_opt":0,"wday":[1,1,1,1,1,1,1],"smin":${minuteOfDay+2},"enable":1,"repeat":1,"etime_opt":-1,"name":"lights off","eact":-1,"sact":0,"emin":0},"set_overall_enable":{"enable":1}}}""")) 62 | pauseExecution(500) 63 | 64 | // and another to power UP 65 | sendCmd(outputXOR("""{"schedule":{"add_rule":{"stime_opt":0,"wday":[1,1,1,1,1,1,1],"smin":${minuteOfDay+3},"enable":1,"repeat":1,"etime_opt":-1,"name":"lights on","eact":-1,"sact":1,"emin":0},"set_overall_enable":{"enable":1}}}""")) 66 | pauseExecution(500) 67 | 68 | // fetch the schedule to see that plug got the commands 69 | sendCmd(outputXOR("""{"schedule":{"get_rules":null}}""")) 70 | 71 | // don't reboot the hub tomorrow again 72 | runIn(600, "resetPlugSchedule") 73 | 74 | // shut the hub down 75 | if (shutdownHub) { 76 | httpPost([uri: "http://127.0.0.1:8080/hub/shutdown"]) { 77 | log.info "hub shutdown initiated" 78 | } 79 | } 80 | } 81 | 82 | private String minutesToReadable(int minutesOfDay) { 83 | java.text.DecimalFormat fmt = new java.text.DecimalFormat("00") 84 | return fmt.format(Math.floorDiv(minutesOfDay, 60)) + ":" + fmt.format(Math.floorMod(minutesOfDay, 60)) 85 | } 86 | 87 | void resetPlugSchedule() { 88 | interfaces.rawSocket.disconnect() 89 | sendCmd(outputXOR("""{"schedule":{"delete_all_rules":null,"erase_runtime_stat":null}}""")) 90 | } 91 | 92 | private void sendCmd(String command) { 93 | if (!getDataValue("deviceIP")) { 94 | log.error("No Kasa plug IP specified") 95 | } else { 96 | if (debug) log.debug("sendCmd: '${inputXOR(command)}'") 97 | try { 98 | if (!interfaces.rawSocket.connected) 99 | interfaces.rawSocket.connect("${getDataValue("deviceIP")}", 9999, byteInterface: true) 100 | 101 | interfaces.rawSocket.sendMessage(command) 102 | } catch (e) { 103 | if (debug) { 104 | log.warn("cannot connect to Kasa plug at ${getDataValue("deviceIP")}: ${getStackTrace(e)}") 105 | } else { 106 | log.warn("cannot connect to Kasa plug at ${getDataValue("deviceIP")}") 107 | } 108 | } 109 | } 110 | } 111 | 112 | void socketStatus(message) { 113 | log.warn("socket status is: ${message}") 114 | } 115 | 116 | def parse(message) { 117 | try { 118 | if (message && debug) log.debug(parseJson(inputXOR(message))) 119 | } catch (Exception e) { 120 | // bad message? 121 | } 122 | } 123 | 124 | private outputXOR(command) { 125 | String encrCmd = "000000" + Integer.toHexString(command.length()) 126 | def key = 0xAB 127 | for (int i = 0; i < command.length(); i++) { 128 | Integer str = (command.charAt(i) as byte) ^ key 129 | key = str 130 | encrCmd += Integer.toHexString(str) 131 | } 132 | return encrCmd 133 | } 134 | 135 | private inputXOR(resp) { 136 | String[] strBytes = resp.substring(8).split("(?<=\\G.{2})") 137 | String cmdResponse = "" 138 | byte key = 0xAB 139 | byte nextKey 140 | byte[] XORtemp 141 | for(int i = 0; i < strBytes.length; i++) { 142 | nextKey = (byte)Integer.parseInt(strBytes[i], 16) // could be negative 143 | XORtemp = nextKey ^ key 144 | key = nextKey 145 | cmdResponse += new String(XORtemp) 146 | } 147 | return cmdResponse 148 | } 149 | -------------------------------------------------------------------------------- /examples/drivers/genericComponentParentDemo.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Generic Component Parent Demo 3 | Copyright 2019 Hubitat Inc. All Rights Reserved 4 | 2019-09-07 (public repo only) maxwell 5 | -initial pub 6 | 7 | */ 8 | 9 | metadata { 10 | definition (name: "Generic Component Parent Demo", namespace: "hubitat", author: "Mike Maxwell") { 11 | capability "Configuration" 12 | 13 | //demo commands, these will create the appropriate component device if it doesn't already exist... 14 | command "childSwitchOn" 15 | command "childSwitchOff" 16 | command "childDimmerOn" 17 | command "childDimmerOff" 18 | command "childDimmerSetLevel", ["number"] 19 | command "setTemperature", ["number"] 20 | 21 | } 22 | preferences { 23 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true 24 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 25 | } 26 | } 27 | 28 | void logsOff(){ 29 | log.warn "debug logging disabled..." 30 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 31 | } 32 | 33 | void updated(){ 34 | log.info "updated..." 35 | log.warn "debug logging is: ${logEnable == true}" 36 | log.warn "description logging is: ${txtEnable == true}" 37 | if (logEnable) runIn(1800,logsOff) 38 | } 39 | 40 | void parse(String description) { 41 | //your parser here... 42 | } 43 | 44 | //demo custom commands 45 | void childSwitchOn(){ 46 | def cd = fetchChild("Switch") 47 | cd.parse([[name:"switch", value:"on", descriptionText:"${cd.displayName} was turned on"]]) 48 | } 49 | 50 | void childSwitchOff(){ 51 | def cd = fetchChild("Switch") 52 | cd.parse([[name:"switch", value:"off", descriptionText:"${cd.displayName} was turned off"]]) 53 | } 54 | 55 | void childDimmerOn(){ 56 | def cd = fetchChild("Dimmer") 57 | List evts = [] 58 | evts.add([name:"switch", value:"on", descriptionText:"${cd.displayName} was turned on"]) 59 | Integer cv = cd.currentValue("level").toInteger() 60 | evts.add([name:"level", value:cv, descriptionText:"${cd.displayName} level was set to ${cv}%", unit: "%"]) 61 | cd.parse(evts) 62 | } 63 | 64 | void childDimmerOff(){ 65 | def cd = fetchChild("Dimmer") 66 | cd.parse([[name:"switch", value:"off", descriptionText:"${cd.displayName} was turned off"]]) 67 | } 68 | 69 | void childDimmerSetLevel(level){ 70 | def cd = fetchChild("Dimmer") 71 | List evts = [] 72 | String cv = cd.currentValue("switch") 73 | if (cv == "off") evts.add([name:"switch", value:"on", descriptionText:"${cd.displayName} was turned on"]) 74 | evts.add([name:"level", value:level, descriptionText:"${cd.displayName} level was set to ${level}%", unit: "%"]) 75 | cd.parse(evts) 76 | } 77 | 78 | void setTemperature(value){ 79 | def cd = fetchChild("Temperature Sensor") 80 | String unit = "°${location.temperatureScale}" 81 | cd.parse([[name:"temperature", value:value, descriptionText:"${cd.displayName} temperature is ${value}${unit}", unit: unit]]) 82 | } 83 | 84 | def fetchChild(String type){ 85 | String thisId = device.id 86 | def cd = getChildDevice("${thisId}-${type}") 87 | if (!cd) { 88 | cd = addChildDevice("hubitat", "Generic Component ${type}", "${thisId}-${type}", [name: "${device.displayName} ${type}", isComponent: true]) 89 | //set initial attribute values, with a real device you would not do this here... 90 | List defaultValues = [] 91 | switch (type) { 92 | case "Switch": 93 | defaultValues.add([name:"switch", value:"off", descriptionText:"set initial switch value"]) 94 | break 95 | case "Dimmer": 96 | defaultValues.add([name:"switch", value:"off", descriptionText:"set initial switch value"]) 97 | defaultValues.add([name:"level", value:50, descriptionText:"set initial level value", unit:"%"]) 98 | break 99 | case "Temperature Sensor" : 100 | String unit = "°${location.temperatureScale}" 101 | BigInteger value = (unit == "°F") ? 70.0 : 21.0 102 | defaultValues.add([name:"temperature", value:value, descriptionText:"set initial temperature value", unit:unit]) 103 | break 104 | default : 105 | log.warn "unable to set initial values for type:${type}" 106 | break 107 | } 108 | cd.parse(defaultValues) 109 | } 110 | return cd 111 | } 112 | 113 | //child device methods 114 | void componentRefresh(cd){ 115 | if (logEnable) log.info "received refresh request from ${cd.displayName}" 116 | } 117 | 118 | void componentOn(cd){ 119 | if (logEnable) log.info "received on request from ${cd.displayName}" 120 | getChildDevice(cd.deviceNetworkId).parse([[name:"switch", value:"on", descriptionText:"${cd.displayName} was turned on"]]) 121 | } 122 | 123 | void componentOff(cd){ 124 | if (logEnable) log.info "received off request from ${cd.displayName}" 125 | getChildDevice(cd.deviceNetworkId).parse([[name:"switch", value:"off", descriptionText:"${cd.displayName} was turned off"]]) 126 | } 127 | 128 | void componentSetLevel(cd,level,transitionTime = null) { 129 | if (logEnable) log.info "received setLevel(${level}, ${transitionTime}) request from ${cd.displayName}" 130 | getChildDevice(cd.deviceNetworkId).parse([[name:"level", value:level, descriptionText:"${cd.displayName} level was set to ${level}%", unit: "%"]]) 131 | } 132 | 133 | void componentStartLevelChange(cd, direction) { 134 | if (logEnable) log.info "received startLevelChange(${direction}) request from ${cd.displayName}" 135 | } 136 | 137 | void componentStopLevelChange(cd) { 138 | if (logEnable) log.info "received stopLevelChange request from ${cd.displayName}" 139 | } 140 | 141 | 142 | List configure() { 143 | log.warn "configure..." 144 | runIn(1800,logsOff) 145 | //your configuration commands here... 146 | } 147 | 148 | -------------------------------------------------------------------------------- /examples/drivers/basicZWaveTool.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Basic Z-Wave tool 3 | 4 | Copyright 2016 -> 2020 Hubitat Inc. All Rights Reserved 5 | 2020-08-14 maxwell 6 | -refactor 7 | -update VersionReport 8 | 2020-08-12 maxwell 9 | -update with S2 support 10 | 2018-11-28 maxwell 11 | -add command hints 12 | 2018-11-09 maxwell 13 | -add association and version reports 14 | 15 | usage: 16 | -replace existing driver with this driver 17 | -set your paremeters 18 | -replace this driver with previous driver 19 | 20 | WARNING! 21 | --Setting device parameters is an advanced feature, randomly poking values to a device 22 | can lead to unexpected results which may result in needing to perform a factory reset 23 | or possibly bricking the device 24 | --Refer to the device documentation for the correct parameters and values for your specific device 25 | --Hubitat cannot be held responsible for misuse of this tool or any unexpected results generated by its use 26 | */ 27 | 28 | import groovy.transform.Field 29 | 30 | metadata { 31 | definition (name: "Basic Z-Wave tool",namespace: "hubitat", author: "Mike Maxwell") { 32 | 33 | command "getAssociationReport" 34 | command "getVersionReport" 35 | command "getCommandClassReport" 36 | command "getParameterReport", [[name:"parameterNumber",type:"NUMBER", description:"Parameter Number (omit for a complete listing of parameters that have been set)", constraints:["NUMBER"]]] 37 | command "setParameter",[[name:"parameterNumber",type:"NUMBER", description:"Parameter Number", constraints:["NUMBER"]],[name:"size",type:"NUMBER", description:"Parameter Size", constraints:["NUMBER"]],[name:"value",type:"NUMBER", description:"Parameter Value", constraints:["NUMBER"]]] 38 | 39 | } 40 | } 41 | 42 | @Field Map zwLibType = [ 43 | 0:"N/A",1:"Static Controller",2:"Controller",3:"Enhanced Slave",4:"Slave",5:"Installer", 44 | 6:"Routing Slave",7:"Bridge Controller",8:"Device Under Test (DUT)",9:"N/A",10:"AV Remote",11:"AV Device" 45 | ] 46 | 47 | void parse(String description) { 48 | hubitat.zwave.Command cmd = zwave.parse(description,[0x85:1,0x86:2]) 49 | if (cmd) { 50 | zwaveEvent(cmd) 51 | } 52 | } 53 | 54 | //Z-Wave responses 55 | void zwaveEvent(hubitat.zwave.commands.versionv2.VersionReport cmd) { 56 | Double firmware0Version = cmd.firmware0Version + (cmd.firmware0SubVersion / 100) 57 | Double protocolVersion = cmd.zWaveProtocolVersion + (cmd.zWaveProtocolSubVersion / 100) 58 | log.info "Version Report - FirmwareVersion: ${firmware0Version}, ProtocolVersion: ${protocolVersion}, HardwareVersion: ${cmd.hardwareVersion}" 59 | device.updateDataValue("firmwareVersion", "${firmware0Version}") 60 | device.updateDataValue("protocolVersion", "${protocolVersion}") 61 | device.updateDataValue("hardwareVersion", "${cmd.hardwareVersion}") 62 | if (cmd.firmwareTargets > 0) { 63 | cmd.targetVersions.each { target -> 64 | Double targetVersion = target.version + (target.subVersion / 100) 65 | device.updateDataValue("firmware${target.target}Version", "${targetVersion}") 66 | } 67 | } 68 | } 69 | 70 | void zwaveEvent(hubitat.zwave.commands.associationv1.AssociationReport cmd) { 71 | log.info "AssociationReport- groupingIdentifier:${cmd.groupingIdentifier}, maxNodesSupported:${cmd.maxNodesSupported}, nodes:${cmd.nodeId}" 72 | } 73 | 74 | void zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) { 75 | log.info "ConfigurationReport- parameterNumber:${cmd.parameterNumber}, size:${cmd.size}, value:${cmd.scaledConfigurationValue}" 76 | } 77 | 78 | void zwaveEvent(hubitat.zwave.commands.versionv1.VersionCommandClassReport cmd) { 79 | log.info "CommandClassReport- class:${ "0x${intToHexStr(cmd.requestedCommandClass)}" }, version:${cmd.commandClassVersion}" 80 | } 81 | 82 | String zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { 83 | hubitat.zwave.Command encapCmd = cmd.encapsulatedCommand() 84 | if (encapCmd) { 85 | return zwaveEvent(encapCmd) 86 | } else { 87 | log.warn "Unable to extract encapsulated cmd from ${cmd}" 88 | } 89 | } 90 | 91 | void zwaveEvent(hubitat.zwave.Command cmd) { 92 | log.debug "skip: ${cmd}" 93 | } 94 | 95 | //cmds 96 | def getVersionReport(){ 97 | return secureCmd(zwave.versionV1.versionGet()) 98 | } 99 | 100 | List setParameter(parameterNumber = null, size = null, value = null){ 101 | if (parameterNumber == null || size == null || value == null) { 102 | log.warn "incomplete parameter list supplied..." 103 | log.info "syntax: setParameter(parameterNumber,size,value)" 104 | } else { 105 | return delayBetween([ 106 | secureCmd(zwave.configurationV1.configurationSet(scaledConfigurationValue: value, parameterNumber: parameterNumber, size: size)), 107 | secureCmd(zwave.configurationV1.configurationGet(parameterNumber: parameterNumber)) 108 | ],500) 109 | } 110 | } 111 | 112 | List getAssociationReport(){ 113 | List cmds = [] 114 | 1.upto(5, { 115 | cmds.add(secureCmd(zwave.associationV1.associationGet(groupingIdentifier: it))) 116 | }) 117 | return delayBetween(cmds,500) 118 | } 119 | 120 | List getParameterReport(param = null){ 121 | List cmds = [] 122 | if (param != null) { 123 | cmds = [secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param))] 124 | } else { 125 | 0.upto(255, { 126 | cmds.add(secureCmd(zwave.configurationV1.configurationGet(parameterNumber: it))) 127 | }) 128 | } 129 | log.trace "configurationGet command(s) sent..." 130 | return delayBetween(cmds,500) 131 | } 132 | 133 | List getCommandClassReport(){ 134 | List cmds = [] 135 | List ic = getDataValue("inClusters").split(",").collect{ hexStrToUnsignedInt(it) } 136 | ic.each { 137 | if (it) cmds.add(secureCmd(zwave.versionV1.versionCommandClassGet(requestedCommandClass:it))) 138 | } 139 | return delayBetween(cmds,500) 140 | } 141 | 142 | String secure(String cmd){ 143 | return zwaveSecureEncap(cmd) 144 | } 145 | 146 | String secure(hubitat.zwave.Command cmd){ 147 | return zwaveSecureEncap(cmd) 148 | } 149 | 150 | void installed(){} 151 | 152 | void configure() {} 153 | 154 | void updated() {} 155 | 156 | String secureCmd(cmd) { 157 | if (getDataValue("zwaveSecurePairingComplete") == "true" && getDataValue("S2") == null) { 158 | return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() 159 | } else { 160 | return secure(cmd) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /examples/drivers/virtualRGBW.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Virtual RGBW Light 3 | 4 | Copyright 2018 -> 2022 Hubitat Inc. All Rights Reserved 5 | 6 | */ 7 | 8 | metadata { 9 | definition (name: "Virtual RGBW Light", namespace: "hubitat", author: "Mike Maxwell") { 10 | capability "Actuator" 11 | capability "Color Control" 12 | capability "Color Temperature" 13 | capability "Switch" 14 | capability "Switch Level" 15 | capability "Light" 16 | capability "ColorMode" 17 | 18 | } 19 | 20 | preferences { 21 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 22 | } 23 | } 24 | 25 | def updated(){} 26 | def installed() { 27 | setColorTemperature(2500) 28 | setColor([hue:50, saturation:100, level:100]) 29 | } 30 | 31 | void noCommands(cmd) { 32 | log.trace "Command ${cmd} is not implemented on this device." 33 | } 34 | 35 | def parse(String description) { noCommands("parse") } 36 | 37 | private eventSend(name,verb,value,unit = ""){ 38 | String descriptionText = "${device.displayName} ${name} ${verb} ${value}${unit}" 39 | if (txtEnable) log.info "${descriptionText}" 40 | if (unit != "") sendEvent(name: name, value: value ,descriptionText: descriptionText, unit:unit) 41 | else sendEvent(name: name, value: value ,descriptionText: descriptionText) 42 | } 43 | 44 | def on() { 45 | String verb = (device.currentValue("switch") == "on") ? "is" : "was turned" 46 | eventSend("switch",verb,"on") 47 | } 48 | 49 | def off() { 50 | String verb = (device.currentValue("switch") == "off") ? "is" : "was turned" 51 | eventSend("switch",verb,"off") 52 | } 53 | 54 | def setLevel(value, rate = null) { 55 | if (value == null) return 56 | Integer level = limitIntegerRange(value,0,100) 57 | if (level == 0) { 58 | off() 59 | return 60 | } 61 | if (device.currentValue("switch") != "on") on() 62 | String verb = (device.currentValue("level") == level) ? "is" : "was set to" 63 | eventSend("level",verb,level,"%") 64 | } 65 | 66 | def setColor(Map value){ 67 | if (value == null) return 68 | if (value.hue == null || value.saturation == null) return 69 | 70 | if (device.currentValue("switch") != "on") on() 71 | 72 | if (device.currentValue("colorMode") != "RGB") { 73 | eventSend("colorMode","is","RGB") 74 | } 75 | Integer hue = limitIntegerRange(value.hue,0,100) 76 | String verb = (device.currentValue("hue") == hue) ? "is" : "was set to" 77 | eventSend("hue",verb,hue,"%") 78 | setGenericName(hue) 79 | 80 | Integer sat = limitIntegerRange(value.saturation,0,100) 81 | verb = (device.currentValue("saturation") == sat) ? "is" : "was set to" 82 | eventSend("saturation",verb,sat,"%") 83 | 84 | if (value.level) { 85 | Integer level = limitIntegerRange(value.level,0,100) 86 | verb = (device.currentValue("level") == level) ? "is" : "was set to" 87 | eventSend("level",verb,level,"%") 88 | } 89 | } 90 | 91 | def setHue(value) { 92 | if (value == null) return 93 | Integer hue = limitIntegerRange(value,0,100) 94 | 95 | if (device.currentValue("switch") != "on") on() 96 | if (device.currentValue("colorMode") != "RGB") { 97 | eventSend("colorMode","is","RGB") 98 | } 99 | String verb = (device.currentValue("hue") == hue) ? "is" : "was set to" 100 | eventSend("hue",verb,hue,"%") 101 | setGenericName(hue) 102 | } 103 | 104 | def setSaturation(value) { 105 | if (value == null) return 106 | Integer sat = limitIntegerRange(value,0,100) 107 | if (device.currentValue("switch") != "on") on() 108 | if (device.currentValue("colorMode") != "RGB") { 109 | eventSend("colorMode","is","RGB") 110 | } 111 | String verb = (device.currentValue("saturation") == sat) ? "is" : "was set to" 112 | eventSend("saturation",verb,sat,"%") 113 | } 114 | 115 | def setColorTemperature(value, level = null, tt = null) { 116 | if (value == null) return 117 | if (level) setLevel(level, tt) 118 | Integer ct = limitIntegerRange(value,2000,6000) 119 | if (device.currentValue("switch") != "on") on() 120 | if (device.currentValue("colorMode") != "CT") { 121 | eventSend("colorMode","is","CT") 122 | } 123 | String verb = (device.currentValue("colorTemperature") == ct) ? "is" : "was set to" 124 | eventSend("colorTemperature",verb,ct,"°K") 125 | setGenericTempName(ct) 126 | } 127 | 128 | 129 | def setGenericTempName(value){ 130 | String genericName 131 | Integer sat = value.toInteger() 132 | if (sat <= 2000) genericName = "Sodium" 133 | else if (sat <= 2100) genericName = "Starlight" 134 | else if (sat < 2400) genericName = "Sunrise" 135 | else if (sat < 2800) genericName = "Incandescent" 136 | else if (sat < 3300) genericName = "Soft White" 137 | else if (sat < 3500) genericName = "Warm White" 138 | else if (sat < 4150) genericName = "Moonlight" 139 | else if (sat <= 5000) genericName = "Horizon" 140 | else if (sat < 5500) genericName = "Daylight" 141 | else if (sat < 6000) genericName = "Electronic" 142 | else if (sat <= 6500) genericName = "Skylight" 143 | else if (sat < 20000) genericName = "Polar" 144 | eventSend("colorName","is",genericName) 145 | } 146 | 147 | def setGenericName(value){ 148 | String colorName 149 | Integer hue = value.toInteger() 150 | if (!hiRezHue) hue = (hue * 3.6).toInteger() 151 | switch (hue){ 152 | case 0..15: colorName = "Red" 153 | break 154 | case 16..45: colorName = "Orange" 155 | break 156 | case 46..75: colorName = "Yellow" 157 | break 158 | case 76..105: colorName = "Chartreuse" 159 | break 160 | case 106..135: colorName = "Green" 161 | break 162 | case 136..165: colorName = "Spring" 163 | break 164 | case 166..195: colorName = "Cyan" 165 | break 166 | case 196..225: colorName = "Azure" 167 | break 168 | case 226..255: colorName = "Blue" 169 | break 170 | case 256..285: colorName = "Violet" 171 | break 172 | case 286..315: colorName = "Magenta" 173 | break 174 | case 316..345: colorName = "Rose" 175 | break 176 | case 346..360: colorName = "Red" 177 | break 178 | } 179 | eventSend("colorName","is",colorName) 180 | } 181 | 182 | Integer limitIntegerRange(value,min,max) { 183 | Integer limit = value.toInteger() 184 | return (limit < min) ? min : (limit > max) ? max : limit 185 | } 186 | -------------------------------------------------------------------------------- /example-apps/lightsUsage.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Light Usage Table 3 | * 4 | * Copyright 2022 Hubitat, Inc. All Rights Reserved. 5 | * 6 | */ 7 | 8 | definition( 9 | name: "Light Usage Table", 10 | namespace: "hubitat", 11 | author: "Bruce Ravenel", 12 | description: "Show Time Usage of Lights", 13 | category: "Convenience", 14 | iconUrl: "", 15 | iconX2Url: "" 16 | ) 17 | 18 | preferences { 19 | page(name: "mainPage") 20 | } 21 | 22 | def mainPage() { 23 | if(state.lights == null) state.lights = [:] 24 | if(state.lightsList == null) state.lightsList = [] 25 | dynamicPage(name: "mainPage", title: "Light Usage Table", uninstall: true, install: true) { 26 | section { 27 | input "lights", "capability.switch", title: "Select Lights to Measure Usage", multiple: true, submitOnChange: true, width: 4 28 | lights.each {dev -> 29 | if(!state.lights["$dev.id"]) { 30 | state.lights["$dev.id"] = [start: dev.currentSwitch == "on" ? now() : 0, total: 0, var: "", time: ""] 31 | state.lightsList += dev.id 32 | } 33 | } 34 | input "resetVar", "enum", title: "Select Boolean Variable to Reset Timers", submitOnChange: true, width: 4, style: 'margin-left:10px', 35 | options: getAllGlobalVars().findAll{it.value.type == "boolean"}.keySet().collect().sort{it.capitalize()} 36 | if(lights) { 37 | if(lights.id.sort() != state.lightsList.sort()) { //something was removed 38 | state.lightsList = lights.id 39 | Map newState = [:] 40 | lights.each{d -> newState["$d.id"] = state.lights["$d.id"]} 41 | state.lights = newState 42 | } 43 | updated() 44 | paragraph displayTable() 45 | if(state.newVar) { 46 | List vars = getAllGlobalVars().findAll{it.value.type == "string"}.keySet().collect().sort{it.capitalize()} 47 | input "newVar", "enum", title: "Select Variable", submitOnChange: true, width: 4, options: vars, newLineAfter: true 48 | if(newVar) { 49 | state.lights[state.newVar].var = newVar 50 | state.remove("newVar") 51 | app.removeSetting("newVar") 52 | paragraph "" 53 | } 54 | } else if(state.remVar) { 55 | state.lights[state.remVar].var = "" 56 | state.remove("remVar") 57 | paragraph "" 58 | } 59 | input "refresh", "button", title: "Refresh Table", width: 2 60 | input "reset", "button", title: "Reset Table", width: 2 61 | } 62 | } 63 | } 64 | } 65 | 66 | String displayTable() { 67 | if(state.reset) { 68 | def dev = lights.find{"$it.id" == state.reset} 69 | state.lights[state.reset].start = dev.currentSwitch == "on" ? now() : 0 70 | state.lights[state.reset].time = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") 71 | state.lights[state.reset].total = 0 72 | state.remove("reset") 73 | } 74 | String str = "" 75 | str += "
" + 77 | "" + 78 | "" + 79 | "" + 80 | "" + 81 | "" 82 | lights.sort{it.displayName.toLowerCase()}.each {dev -> 83 | int total = state.lights["$dev.id"].total / 1000 84 | String thisVar = state.lights["$dev.id"].var 85 | int hours = total / 3600 86 | total = total % 3600 87 | int mins = total / 60 88 | int secs = total % 60 89 | String time = "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs" 90 | if(thisVar) setGlobalVar(thisVar, time) 91 | String devLink = "$dev" 92 | String reset = buttonLink("d$dev.id", "", "black", "20px") 93 | String var = thisVar ? buttonLink("r$dev.id", thisVar, "purple") : buttonLink("n$dev.id", "Select", "green") 94 | str += "" + 95 | "" + 96 | "" + 97 | "" + 98 | "" 99 | } 100 | str += "
LightTotal On TimeResetTime StampVariable
$devLink$time$reset${state.lights["$dev.id"].time ?: ""}$var
" 101 | str 102 | } 103 | 104 | String buttonLink(String btnName, String linkText, color = "#1A77C9", font = "15px") { 105 | "
$linkText
" 106 | } 107 | 108 | void appButtonHandler(btn) { 109 | if(btn == "reset") resetTimers() 110 | else if(btn == "refresh") state.lights.each{k, v -> 111 | def dev = lights.find{"$it.id" == k} 112 | if(dev.currentSwitch == "on") { 113 | state.lights[k].total += now() - state.lights[k].start 114 | state.lights[k].start = now() 115 | } 116 | } else if(btn.startsWith("n")) state.newVar = btn.minus("n") 117 | else if(btn.startsWith("r")) state.remVar = btn.minus("r") 118 | else state.reset = btn.minus("d") 119 | } 120 | 121 | def updated() { 122 | unsubscribe() 123 | initialize() 124 | } 125 | 126 | def installed() { 127 | } 128 | 129 | void initialize() { 130 | subscribe(lights, "switch.on", onHandler) 131 | subscribe(lights, "switch.off", offHandler) 132 | if(resetVar) { 133 | subscribe(location, "variable:${resetVar}.true", resetTimers) 134 | setGlobalVar(resetVar, false) 135 | } 136 | } 137 | 138 | void onHandler(evt) { 139 | state.lights[evt.device.id].start = now() 140 | } 141 | 142 | void offHandler(evt) { 143 | state.lights[evt.device.id].total += now() - state.lights[evt.device.id].start 144 | String thisVar = state.lights[evt.device.id].var 145 | if(thisVar) { 146 | int total = state.lights[evt.device.id].total / 1000 147 | int hours = total / 3600 148 | total = total % 3600 149 | int mins = total / 60 150 | int secs = total % 60 151 | setGlobalVar(thisVar, "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs") 152 | } 153 | } 154 | 155 | void resetTimers(evt = null) { 156 | state.lights.each{k, v -> 157 | def dev = lights.find{"$it.id" == k} 158 | state.lights[k].start = dev.currentSwitch == "on" ? now() : 0 159 | state.lights[k].time = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") 160 | state.lights[k].total = 0 161 | } 162 | if(resetVar) setGlobalVar(resetVar, false) 163 | } 164 | -------------------------------------------------------------------------------- /smartapps/hubitat/Send_Hub_Events.src/Send_Hub_Events.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017, 2018 Hubitat, Inc. All Rights Reserved. 3 | * 4 | * This software if free for Private Use. You may use and modify the software without distributing it. 5 | * You may not grant a sublicense to modify and distribute this software to third parties. 6 | * Software is provided without warranty and your use of it is at your own risk. 7 | * 8 | * NOTE: This "App" is for use on the SmartThings platform, it does not work on Hubitat. It will send 9 | * events from the SmartThings system to Hubitat once it is installed. 10 | */ 11 | definition( 12 | name: "Send Hub Events", 13 | namespace: "hubitat", 14 | author: "Charles Schwer, Mike Maxwell and Bruce Ravenel", 15 | description: "Send events to Hubitat™ Elevation Hub", 16 | category: "Convenience", 17 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 18 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 19 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" 20 | ) 21 | 22 | preferences { 23 | page(name: "main") 24 | } 25 | 26 | def main(){ 27 | dynamicPage(name: "main", title: "Send Hub Events", uninstall: true, install: true){ 28 | section { 29 | input "ip", "text", title:"Hubitat™ Elevation Hub IP", required: true 30 | } 31 | section { 32 | input "enabled", "bool", title: "Enable Hub Link?", required: false, defaultValue: true 33 | } 34 | section("Monitor these devices...") { 35 | input "presenceDevices", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false 36 | input "motionDevices", "capability.motionSensor", title: "Motion Sensors (motion, temperature)", multiple: true, required: false 37 | input "contactDevices", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false 38 | input "accelerationDevices", "capability.accelerationSensor", title: "Acceleration Sensors", multiple: true, required: false 39 | input "multiSensors", "capability.contactSensor", title: "Multi Sensors (contact, acceleration, temperature)", multiple: true, required: false 40 | input "omniSensors", "capability.sensor", title: "Omni Sensors (presence, contact, acceleration, temperature, carbonMonoxide, illuminance, motion, water, smoke)", multiple: true, required: false 41 | input "switchDevices", "capability.switch", title: "Switches", multiple: true, required: false 42 | input "dimmerDevices", "capability.switchLevel", title: "Dimmers", multiple: true, required: false 43 | input "locks", "capability.lock", title: "Locks", multiple: true, required: false 44 | input "modes", "bool", title: "Send mode changes?", required: false 45 | input "logEnable", "bool", title: "Enable debug logging", required: false 46 | } 47 | } 48 | } 49 | 50 | def installed() { 51 | initialize() 52 | } 53 | 54 | 55 | def updated() { 56 | unsubscribe() 57 | initialize() 58 | } 59 | 60 | 61 | def initialize() { 62 | subscribe(presenceDevices, "presence", handleDeviceEvent) 63 | subscribe(motionDevices, "motion", handleDeviceEvent) 64 | subscribe(motionDevices, "temperature", handleDeviceEvent) 65 | subscribe(contactDevices, "contact", handleDeviceEvent) 66 | subscribe(accelerationDevices, "acceleration", handleDeviceEvent) 67 | subscribe(multiSensors, "contact", handleDeviceEvent) 68 | subscribe(multiSensors, "acceleration", handleDeviceEvent) 69 | subscribe(multiSensors, "temperature", handleDeviceEvent) 70 | subscribe(omniSensors, "presence", omniDeviceEvent) 71 | subscribe(omniSensors, "contact", omniDeviceEvent) 72 | subscribe(omniSensors, "acceleration", omniDeviceEvent) 73 | subscribe(omniSensors, "temperature", omniDeviceEvent) 74 | subscribe(omniSensors, "carbonMonoxide", omniDeviceEvent) 75 | subscribe(omniSensors, "illuminance", omniDeviceEvent) 76 | subscribe(omniSensors, "motion", omniDeviceEvent) 77 | subscribe(omniSensors, "water", omniDeviceEvent) 78 | subscribe(omniSensors, "smoke", omniDeviceEvent) 79 | subscribe(omniSensors, "humidity", omniDeviceEvent) 80 | subscribe(omniSensors, "carbonDioxide", omniDeviceEvent) 81 | subscribe(switchDevices, "switch", handleDeviceEvent) 82 | subscribe(dimmerDevices, "switch", handleDeviceEvent) 83 | subscribe(dimmerDevices, "level", handleDeviceEvent) 84 | subscribe(locks, "lock", handleDeviceEvent) 85 | if(modes) subscribe(location, modeEvent) 86 | sendSetup() 87 | } 88 | 89 | def handleDeviceEvent(evt) { 90 | def dni = "stHub_${evt?.device?.deviceNetworkId}" 91 | def msg = """POST / HTTP/1.1 92 | HOST: ${ip}:39501 93 | CONTENT-TYPE: text/plain 94 | DEVICE-NETWORK-ID: ${dni} 95 | CONTENT-LENGTH: ${evt.value.length()}\n 96 | ${evt.value} 97 | """ 98 | if(enabled) { 99 | if (logEnable) log.debug "Name: ${evt.device.displayName}, DNI: ${dni}, value: ${evt.value}" 100 | sendHubCommand(new physicalgraph.device.HubAction(msg, physicalgraph.device.Protocol.LAN, "${ip}:39501")) 101 | } 102 | } 103 | 104 | def omniDeviceEvent(evt) { 105 | def dni = "stHub_${evt?.device?.deviceNetworkId}" 106 | def msg = """POST / HTTP/1.1 107 | HOST: ${ip}:39501 108 | CONTENT-TYPE: text/plain 109 | DEVICE-NETWORK-ID: ${dni} 110 | CONTENT-LENGTH: ${(evt.name.length() + evt.value.length() + 1)}\n 111 | ${evt.name}:${evt.value} 112 | """ 113 | if(enabled) { 114 | if (logEnable) log.debug "Name: ${evt.device.displayName}, DNI: ${dni}, name: ${evt.name} value: ${evt.value}" 115 | sendHubCommand(new physicalgraph.device.HubAction(msg, physicalgraph.device.Protocol.LAN, "${ip}:39501")) 116 | } 117 | } 118 | 119 | def sendSetup() { 120 | def thisMsg = "" 121 | presenceDevices.each {thisMsg = thisMsg + "p\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 122 | motionDevices.each {thisMsg = thisMsg + "m\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 123 | contactDevices.each {thisMsg = thisMsg + "c\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 124 | accelerationDevices.each {thisMsg = thisMsg + "a\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 125 | multiSensors.each {thisMsg = thisMsg + "x\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 126 | omniSensors.each {thisMsg = thisMsg + "o\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 127 | switchDevices.each {thisMsg = thisMsg + "s\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 128 | dimmerDevices.each {thisMsg = thisMsg + "d\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 129 | locks.each {thisMsg = thisMsg + "l\t$it.displayName\tstHub_$it.deviceNetworkId\n"} 130 | def dni = "systemHubLink" 131 | def msg = """POST / HTTP/1.1 132 | HOST: ${ip}:39501 133 | CONTENT-TYPE: text/plain 134 | DEVICE-NETWORK-ID: ${dni} 135 | CONTENT-LENGTH: ${thisMsg.length()}\n 136 | ${thisMsg} 137 | """ 138 | if(enabled) { 139 | if (logEnable) log.debug "Setup: $msg" 140 | sendHubCommand(new physicalgraph.device.HubAction(msg, physicalgraph.device.Protocol.LAN, "${ip}:39501")) 141 | } 142 | } 143 | 144 | def modeEvent(evt){ 145 | if(evt.name != "mode") return 146 | def dni = "stHub_mode_" 147 | def msg = """POST / HTTP/1.1 148 | HOST: ${ip}:39501 149 | CONTENT-TYPE: text/plain 150 | DEVICE-NETWORK-ID: ${dni} 151 | CONTENT-LENGTH: ${evt.value.length()}\n 152 | ${evt.value} 153 | """ 154 | if(enabled) { 155 | if (logEnable) log.debug "Name: Mode, value: ${evt.value}" 156 | sendHubCommand(new physicalgraph.device.HubAction(msg, physicalgraph.device.Protocol.LAN, "${ip}:39501")) 157 | } 158 | } 159 | 160 | def uninstalled() { 161 | removeChildDevices(getChildDevices()) 162 | } 163 | 164 | private removeChildDevices(delete) { 165 | delete.each {deleteChildDevice(it.deviceNetworkId)} 166 | } 167 | -------------------------------------------------------------------------------- /examples/drivers/virtualOmniSensor.groovy: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2019 Hubitat Inc. All Rights Reserved 2 | 3 | metadata { 4 | definition (name: "Virtual Omni Sensor", namespace: "hubitat", author: "Bruce Ravenel") { 5 | capability "Presence Sensor" 6 | capability "Acceleration Sensor" 7 | capability "Carbon Dioxide Measurement" 8 | capability "Carbon Monoxide Detector" 9 | capability "Contact Sensor" 10 | capability "Illuminance Measurement" 11 | capability "Motion Sensor" 12 | capability "Relative Humidity Measurement" 13 | capability "Smoke Detector" 14 | capability "Temperature Measurement" 15 | capability "Water Sensor" 16 | capability "Energy Meter" 17 | capability "Power Meter" 18 | command "arrived" 19 | command "departed" 20 | command "accelerationActive" 21 | command "accelerationInactive" 22 | command "motionActive" 23 | command "motionInactive" 24 | command "open" 25 | command "close" 26 | command "CODetected" 27 | command "COClear" 28 | command "smokeDetected" 29 | command "smokeClear" 30 | command "setCarbonDioxide", ["Number"] 31 | command "setIlluminance", ["Number"] 32 | command "setRelativeHumidity", ["Number"] 33 | command "setTemperature", ["Number"] 34 | command "wet" 35 | command "dry" 36 | command "setVariable", ["String"] 37 | command "setEnergy", ["Number"] 38 | command "setPower", ["Number"] 39 | attribute "variable", "String" 40 | } 41 | preferences { 42 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true 43 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 44 | } 45 | } 46 | 47 | def logsOff(){ 48 | log.warn "debug logging disabled..." 49 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 50 | } 51 | 52 | def installed() { 53 | log.warn "installed..." 54 | arrived() 55 | accelerationInactive() 56 | COClear() 57 | close() 58 | setIlluminance(50) 59 | setCarbonDioxide(350) 60 | setRelativeHumidity(35) 61 | motionInactive() 62 | smokeClear() 63 | setTemperature(70) 64 | dry() 65 | runIn(1800,logsOff) 66 | } 67 | 68 | def updated() { 69 | log.info "updated..." 70 | log.warn "debug logging is: ${logEnable == true}" 71 | log.warn "description logging is: ${txtEnable == true}" 72 | if (logEnable) runIn(1800,logsOff) 73 | } 74 | 75 | def parse(String description) { 76 | } 77 | 78 | def arrived() { 79 | def descriptionText = "${device.displayName} has arrived" 80 | if (txtEnable) log.info "${descriptionText}" 81 | sendEvent(name: "presence", value: "present",descriptionText: descriptionText) 82 | } 83 | 84 | def departed() { 85 | def descriptionText = "${device.displayName} has departed" 86 | if (txtEnable) log.info "${descriptionText}" 87 | sendEvent(name: "presence", value: "not present",descriptionText: descriptionText) 88 | } 89 | 90 | def accelerationActive() { 91 | def descriptionText = "${device.displayName} is active" 92 | if (txtEnable) log.info "${descriptionText}" 93 | sendEvent(name: "acceleration", value: "active", descriptionText: descriptionText) 94 | } 95 | 96 | def accelerationInactive() { 97 | def descriptionText = "${device.displayName} is inactive" 98 | if (txtEnable) log.info "${descriptionText}" 99 | sendEvent(name: "acceleration", value: "inactive", descriptionText: descriptionText) 100 | } 101 | 102 | def CODetected() { 103 | def descriptionText = "${device.displayName} CO detected" 104 | if (txtEnable) log.info "${descriptionText}" 105 | sendEvent(name: "carbonMonoxide", value: "detected", descriptionText: descriptionText) 106 | } 107 | 108 | def COClear() { 109 | def descriptionText = "${device.displayName} CO clear" 110 | if (txtEnable) log.info "${descriptionText}" 111 | sendEvent(name: "carbonMonoxide", value: "clear", descriptionText: descriptionText) 112 | } 113 | 114 | def open() { 115 | def descriptionText = "${device.displayName} is open" 116 | if (txtEnable) log.info "${descriptionText}" 117 | sendEvent(name: "contact", value: "open", descriptionText: descriptionText) 118 | } 119 | 120 | def close() { 121 | def descriptionText = "${device.displayName} is closed" 122 | if (txtEnable) log.info "${descriptionText}" 123 | sendEvent(name: "contact", value: "closed", descriptionText: descriptionText) 124 | } 125 | 126 | def setIlluminance(lux) { 127 | def descriptionText = "${device.displayName} is ${lux} lux" 128 | if (txtEnable) log.info "${descriptionText}" 129 | sendEvent(name: "illuminance", value: lux, descriptionText: descriptionText, unit: "Lux") 130 | } 131 | 132 | def setCarbonDioxide(CO2) { 133 | def descriptionText = "${device.displayName} Carbon Dioxide is ${CO2} ppm" 134 | if (txtEnable) log.info "${descriptionText}" 135 | sendEvent(name: "carbonDioxide", value: CO2, descriptionText: descriptionText, unit: "ppm") 136 | } 137 | 138 | def setRelativeHumidity(humid) { 139 | def descriptionText = "${device.displayName} is ${humid}% humidity" 140 | if (txtEnable) log.info "${descriptionText}" 141 | sendEvent(name: "humidity", value: humid, descriptionText: descriptionText, unit: "RH%") 142 | } 143 | 144 | def smokeDetected() { 145 | def descriptionText = "${device.displayName} smoke detected" 146 | if (txtEnable) log.info "${descriptionText}" 147 | sendEvent(name: "smoke", value: "detected", descriptionText: descriptionText) 148 | } 149 | 150 | def motionActive() { 151 | def descriptionText = "${device.displayName} is active" 152 | if (txtEnable) log.info "${descriptionText}" 153 | sendEvent(name: "motion", value: "active", descriptionText: descriptionText) 154 | } 155 | 156 | def motionInactive() { 157 | def descriptionText = "${device.displayName} is inactive" 158 | if (txtEnable) log.info "${descriptionText}" 159 | sendEvent(name: "motion", value: "inactive", descriptionText: descriptionText) 160 | } 161 | 162 | def smokeClear() { 163 | def descriptionText = "${device.displayName} smoke clear" 164 | if (txtEnable) log.info "${descriptionText}" 165 | sendEvent(name: "smoke", value: "clear", descriptionText: descriptionText) 166 | } 167 | 168 | def setTemperature(temp) { 169 | def unit = "°${location.temperatureScale}" 170 | def descriptionText = "${device.displayName} is ${temp}${unit}" 171 | if (txtEnable) log.info "${descriptionText}" 172 | sendEvent(name: "temperature", value: temp, descriptionText: descriptionText, unit: unit) 173 | } 174 | 175 | def wet() { 176 | def descriptionText = "${device.displayName} water wet" 177 | if (txtEnable) log.info "${descriptionText}" 178 | sendEvent(name: "water", value: "wet", descriptionText: descriptionText) 179 | } 180 | 181 | def dry() { 182 | def descriptionText = "${device.displayName} water dry" 183 | if (txtEnable) log.info "${descriptionText}" 184 | sendEvent(name: "water", value: "dry", descriptionText: descriptionText) 185 | } 186 | 187 | def setVariable(str) { 188 | def descriptionText = "${device.displayName} variable is ${str}" 189 | if (txtEnable) log.info "${descriptionText}" 190 | sendEvent(name: "variable", value: str, descriptionText: descriptionText) 191 | } 192 | 193 | def setEnergy(energy) { 194 | def descriptionText = "${device.displayName} is ${energy} energy" 195 | if (txtEnable) log.info "${descriptionText}" 196 | sendEvent(name: "energy", value: energy, descriptionText: descriptionText) 197 | } 198 | 199 | def setPower(power) { 200 | def descriptionText = "${device.displayName} is ${power} power" 201 | if (txtEnable) log.info "${descriptionText}" 202 | sendEvent(name: "power", value: power, descriptionText: descriptionText) 203 | } 204 | -------------------------------------------------------------------------------- /examples/drivers/neeoRemote.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | NEEO Remote 3 | 4 | Copyright 2016, 2017, 2018 Hubitat Inc. All Rights Reserved 5 | 6 | 2018-12-27 2.0.3 maxwell 7 | -add component switch option 8 | -implement set and clear for neeo brain forwarding 9 | 2018-12-24 2.0.3 maxwell 10 | -initial pub 11 | 12 | */ 13 | 14 | metadata { 15 | definition (name: "NEEO Remote", namespace: "hubitat", author: "Mike Maxwell") { 16 | capability "MediaController" 17 | command "refresh" 18 | } 19 | preferences { 20 | input name: "ip", type: "text", title: "NEEO ip address" 21 | input name: "activityChildren", type: "bool", title: "Create Activity Switches", defaultValue: false 22 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true 23 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 24 | } 25 | } 26 | 27 | def logsOff(){ 28 | log.warn "debug logging disabled..." 29 | device.updateSetting("logEnable",[value:false,type:"bool"]) 30 | } 31 | 32 | def updated(){ 33 | log.info "updated..." 34 | log.warn "debug logging is: ${logEnable == true}" 35 | log.warn "description logging is: ${txtEnable == true}" 36 | if (logEnable) runIn(1800,logsOff) 37 | if (ip) { 38 | def quad = ip.split(/\./) 39 | def hexIP = "" 40 | quad.each { 41 | hexIP+= Integer.toHexString(it.toInteger()).padLeft(2,"0").toUpperCase() 42 | } 43 | if (device.deviceNetworkId != hexIP) { 44 | device.setDeviceNetworkId(hexIP) 45 | } 46 | setForwarding() 47 | if (activityChildren) { 48 | deviceSync(state.activities) 49 | } else { 50 | getChildDevices().each { deleteChildDevice("${it.deviceNetworkId}") } 51 | } 52 | getAllActivities() 53 | } else log.error "no ip address set, please set an IP address." 54 | } 55 | 56 | private deviceSync(parsed) { 57 | def dni 58 | getChildDevices().each{ 59 | dni = it.deviceNetworkId 60 | if (!(parsed.find{ it.value.scenerio == dni })) { 61 | deleteChildDevice("${dni}") 62 | } 63 | } 64 | parsed.each { d -> 65 | dni = "${d.value.scenerio}" 66 | def cd = getChildDevice("${dni}") 67 | if (!cd) { 68 | cd = addChildDevice("hubitat", "Generic Component Switch", "${dni}", [label: "${d.key}", name: "${d.key}", isComponent: true]) 69 | if (cd && logEnable) { 70 | log.debug "Activity device ${cd.displayName} was created" 71 | } else if (!cd) { 72 | log.error "error creating device" 73 | } 74 | } //else log.info "device already exists" 75 | } 76 | } 77 | 78 | def installed() { 79 | log.debug "installed" 80 | device.updateSetting("txtEnable",[type:"bool", value: true]) 81 | } 82 | 83 | def uninstalled(){ 84 | log.trace "uninstalled" 85 | clearForwarding() 86 | getChildDevices().each { deleteChildDevice("${it.deviceNetworkId}") } 87 | } 88 | 89 | def startActivity(name) { 90 | def uri = state.activities."${name}"?.on 91 | if (uri) { 92 | generalGet(uri) 93 | } 94 | } 95 | 96 | private clearForwarding() { 97 | def params = [ 98 | uri: "http://${ip}:3000", 99 | path: "/v1/forwardactions/", 100 | headers: [ 101 | "Content-Type": "application/json;charset=UTF-8" 102 | ], 103 | body: """{}""" 104 | ] 105 | generalPost(params) 106 | } 107 | 108 | private setForwarding() { 109 | def params = [ 110 | uri: "http://${ip}:3000", 111 | path: "/v1/forwardactions/", 112 | headers: [ 113 | "Content-Type": "application/json;charset=UTF-8" 114 | ], 115 | body: """{"host":"${location.hubs[0].getDataValue("localIP")}","port":39501}""" 116 | ] 117 | generalPost(params) 118 | } 119 | 120 | private generalPost(params) { 121 | try{ 122 | httpPost(params) { resp -> 123 | if (logEnable) log.debug "generalPost:${resp.data}" 124 | } 125 | } catch(e){ 126 | log.error "generalPost error:${e}" 127 | } 128 | } 129 | 130 | private generalGet(uri){ 131 | def params = [ 132 | uri:uri 133 | ] 134 | try{ 135 | httpGet(params) { resp -> 136 | if (logEnable) log.debug "generalGet:${resp.data}" 137 | if (resp.data?.estimatedDuration) state.execDelay = ((resp.data.estimatedDuration / 1000) + 1).toInteger() 138 | } 139 | } catch(e){ 140 | log.error "generalGet error:${e}" 141 | } 142 | } 143 | 144 | def getCurrentActivity(){ 145 | getAllActivities() 146 | } 147 | 148 | def getAllActivities(){ 149 | def activities = [:] 150 | def currentActivity 151 | def params = [ 152 | uri: "http://${ip}:3000" 153 | ,path: "/v1/api/recipes" 154 | ] 155 | try { 156 | httpGet(params) { resp -> 157 | resp.data.each{ 158 | def name = URLDecoder.decode(it.detail.devicename) 159 | def actOn = it.url.setPowerOn 160 | def actOff = it.url.setPowerOff 161 | activities << ["${name}":["on":actOn,"off":actOff,"scenerio":it.powerKey]] 162 | } 163 | state.activities = activities 164 | sendEvent(name:"activities", value: new groovy.json.JsonBuilder(activities.collect{ it.key }) ) 165 | } 166 | } catch(e){ 167 | log.error "getAllActivities error:${e}" 168 | } 169 | 170 | } 171 | 172 | def parse(String description) { 173 | def msg = parseLanMessage(description) 174 | if (logEnable) log.debug "parse json:${msg.json}" 175 | if (msg.body) { 176 | def result = msg.json 177 | if (result.recipe){ 178 | def action = result.action == "launch" ? "on" : "off" 179 | def activity = result.recipe 180 | if (activityChildren) { 181 | def cd = getChildDevice("${state.activities."${activity}".scenerio}") 182 | if (cd) cd.parse([[name:"switch",value:action,descriptionText:"Neeo activity ${activity} was turned ${action}"]]) 183 | } 184 | if (action == "off") activity = "None" 185 | if (logEnable) log.trace "activity:${activity}, result.action:${result.action}, action:${action}" 186 | sendEvent(name:"currentActivity", value: activity) 187 | runIn(state.execDelay ?: 1,"refresh") 188 | } 189 | } 190 | } 191 | 192 | def refresh() { 193 | def cd 194 | def activeScenerios = [] 195 | def inactiveScenerios = [] 196 | def currentActivity = "None" 197 | def descriptionText = "${device.displayName} current activity is" 198 | def params = [ uri:"http://${ip}:3000/v1/api/activeRecipes" ] 199 | try{ 200 | httpGet(params) { resp -> 201 | resp.data.each { act -> 202 | activeScenerios.add(act) 203 | } 204 | } 205 | inactiveScenerios = state.activities.collect{ it.value.scenerio } - activeScenerios 206 | inactiveScenerios.each { 207 | if (activityChildren) { 208 | cd = getChildDevice(it) 209 | if (cd && cd.currentValue("switch") != "off") { 210 | cd.parse([[name:"switch",value:"off",descriptionText:"Neeo activity ${cd.displayName} was turned off"]]) 211 | } 212 | } 213 | } 214 | activeScenerios.each { scenerio -> 215 | if (activityChildren) { 216 | cd = getChildDevice(scenerio) 217 | if (cd && cd.currentValue("switch") != "on") { 218 | cd.parse([[name:"switch",value:"on",descriptionText:"Neeo activity ${cd.displayName} was turned on"]]) 219 | } 220 | } 221 | currentActivity = state.activities.find{ it.value.scenerio == scenerio }.key 222 | } 223 | descriptionText = "${descriptionText} ${currentActivity}" 224 | if (txtEnable) log.info descriptionText 225 | sendEvent(name:"currentActivity", value: currentActivity, descriptionText: descriptionText) 226 | } catch(e){ 227 | log.error "refresh error:${e}" 228 | } 229 | } 230 | 231 | def componentRefresh(cd) { 232 | refresh() 233 | } 234 | 235 | def componentOn(cd) { 236 | def uri = state.activities.find{ it.value.scenerio == cd.deviceNetworkId }.value.on 237 | if (uri) generalGet(uri) 238 | } 239 | 240 | def componentOff(cd) { 241 | def uri = state.activities.find{ it.value.scenerio == cd.deviceNetworkId }.value.off 242 | if (uri) generalGet(uri) 243 | } 244 | -------------------------------------------------------------------------------- /examples/drivers/environmentSensor.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Environment Sensor 3 | 4 | 2018-08-09 maxwell 5 | -cleaned up parsing and eventing 6 | */ 7 | 8 | import groovy.transform.Field 9 | 10 | @Field Map diagAttributes = [ 11 | "0000":["name":"ResetCount","val":0x0000], 12 | "0104":["name":"TXRetrys","val":0x0104], 13 | "0105":["name":"TXFails","val":0x0105], 14 | "011A":["name":"PacketDrops","val":0x011A], 15 | "0115":["name":"DecryptFailures","val":0x0115], 16 | "011D":["name":"RSSI","val":0x011D], 17 | "011E":["name":"Parent","val":0x011E], 18 | "011F":["name":"Children","val":0x011F], 19 | "0120":["name":"Neighbors","val":0x0120] 20 | ] 21 | 22 | metadata { 23 | definition (name: "Environment Sensor", namespace: "iharyadi", author: "iharyadi/maxwell") { 24 | capability "Configuration" 25 | capability "Refresh" 26 | capability "Temperature Measurement" 27 | capability "RelativeHumidityMeasurement" 28 | capability "Illuminance Measurement" 29 | capability "PressureMeasurement" 30 | capability "Sensor" 31 | capability "Switch" 32 | 33 | fingerprint profileId: "0104", inClusters: "0000,0003,0006,0402,0403,0405,0400,0B05", manufacturer: "KMPCIL", model: "RES001BME280", deviceJoinName: "Environment Sensor" 34 | /* 35 | Manufacturer: KMPCIL 36 | Product Name: Environment Sensor 37 | Model Number: RES001BME280 38 | deviceTypeId: 1373 39 | manufacturer:KMPCIL 40 | address64bit:00124B00179E42B8 41 | address16bit:117B 42 | model:RES001BME280 43 | basicAttributesInitialized:true 44 | application:01 45 | endpoints.08.manufacturer:KMPCIL 46 | endpoints.08.idAsInt:8 47 | endpoints.08.inClusters:0000,0003,0006,0402,0403,0405,0400,0B05 48 | endpoints.08.endpointId:08 49 | endpoints.08.profileId:0104 50 | endpoints.08.application:01 51 | endpoints.08.outClusters:0000 52 | endpoints.08.initialized:true 53 | endpoints.08.model:RES001BME280 54 | endpoints.08.stage:4 55 | endpoints.F2.manufacturer:null 56 | endpoints.F2.idAsInt:242 57 | endpoints.F2.inClusters:null 58 | endpoints.F2.endpointId:F2 59 | endpoints.F2.profileId:A1E0 60 | endpoints.F2.application:null 61 | endpoints.F2.outClusters:0021 62 | endpoints.F2.initialized:true 63 | endpoints.F2.model:null 64 | endpoints.F2.stage:4 65 | */ 66 | } 67 | 68 | preferences { 69 | //standard logging options 70 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true 71 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 72 | 73 | //TODO: implement sensor adjustments, suggest doing so using reference values vs offsets 74 | //input "refTemp", "decimal", title: "Reference temperature", description: "Enter current reference temperature reading", range: "*..*" 75 | 76 | //input "tempOffset", "decimal", title: "Degrees", description: "Adjust temperature by this many degrees in Celcius",range: "*..*" 77 | //input "tempFilter", "decimal", title: "Coeficient", description: "Temperature filter between 0.0 and 1.0",range: "*..*" 78 | //input "humOffset", "decimal", title: "Percent", description: "Adjust humidity by this many percent",range: "*..*" 79 | //input "illumAdj", "decimal", title: "Factor", description: "Adjust illuminace base on formula illum / Factor", range: "1..*" 80 | } 81 | } 82 | 83 | def logsOff(){ 84 | log.warn "debug logging disabled..." 85 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 86 | } 87 | 88 | def parse(String description) { 89 | if (logEnable) log.debug "description is ${description}" 90 | if (description.startsWith("catchall")) return 91 | def descMap = zigbee.parseDescriptionAsMap(description) 92 | if (logEnable) log.debug "descMap:${descMap}" 93 | 94 | def cluster = descMap.cluster 95 | def hexValue = descMap.value 96 | def attrId = descMap.attrId 97 | 98 | switch (cluster){ 99 | case "0400" : //illuminance 100 | getLuminanceResult(hexValue) 101 | break 102 | case "0402" : //temp 103 | getTemperatureResult(hexValue) 104 | break 105 | case "0403" : //pressure 106 | getPressureResult(hexValue) 107 | break 108 | case "0405" : //humidity 109 | getHumidityResult(hexValue) 110 | break 111 | case "0B05" : //diag 112 | if (logEnable) log.warn "attrId:${attrId}, hexValue:${hexValue}" 113 | def value = hexStrToUnsignedInt(hexValue) 114 | log.warn "diag- ${diagAttributes."${attrId}".name}:${value} " 115 | break 116 | default : 117 | log.warn "skipped cluster: ${cluster}, descMap:${descMap}" 118 | break 119 | } 120 | return 121 | } 122 | 123 | //event methods 124 | private getTemperatureResult(hex){ 125 | def valueRaw = hexStrToSignedInt(hex) 126 | valueRaw = valueRaw / 100 127 | def value = convertTemperatureIfNeeded(valueRaw.toFloat(),"c",1) 128 | /* 129 | //example temp offset 130 | state.sensorTemp = value 131 | if (state.tempOffset) { 132 | value = (value.toFloat() + state.tempOffset.toFloat()).round(2) 133 | } 134 | */ 135 | def name = "temperature" 136 | def unit = "°${location.temperatureScale}" 137 | def descriptionText = "${device.displayName} ${name} is ${value}${unit}" 138 | if (txtEnable) log.info "${descriptionText}" 139 | sendEvent(name: name,value: value,descriptionText: descriptionText,unit: unit) 140 | } 141 | 142 | private getHumidityResult(hex){ 143 | def valueRaw = hexStrToUnsignedInt(hex) 144 | def value = valueRaw / 100 145 | def name = "humidity" 146 | def unit = "%" 147 | def descriptionText = "${device.displayName} ${name} is ${value}${unit}" 148 | if (txtEnable) log.info "${descriptionText}" 149 | sendEvent(name: name,value: value,descriptionText: descriptionText,unit: unit) 150 | } 151 | 152 | private getLuminanceResult(hex) { 153 | def rawValue = hexStrToUnsignedInt(hex) 154 | def value = (Math.pow(10,(rawValue/10000))+ 1).toInteger() 155 | if (rawValue.toInteger() == 0) value = "0" 156 | def name = "illuminance" 157 | def unit = "Lux" 158 | def descriptionText = "${device.displayName} ${name} is ${value}${unit}" 159 | if (txtEnable) log.info "${descriptionText}" 160 | sendEvent(name: name,value: value,descriptionText: descriptionText,unit: unit) 161 | } 162 | 163 | private getPressureResult(hex){ 164 | def valueRaw = hexStrToUnsignedInt(hex) 165 | def value = valueRaw / 10 166 | def name = "pressure" 167 | def unit = "kPa" 168 | def descriptionText = "${device.displayName} ${name} is ${value}${unit}" 169 | if (txtEnable) log.info "${descriptionText}" 170 | sendEvent(name: name,value: value,descriptionText: descriptionText,unit: unit) 171 | } 172 | 173 | 174 | //capability and device methods 175 | def off() { 176 | zigbee.off() 177 | } 178 | 179 | def on() { 180 | zigbee.on() 181 | } 182 | 183 | def refresh() { 184 | log.debug "Refresh" 185 | 186 | //readAttribute(cluster,attribute,mfg code,optional delay ms) 187 | def cmds = zigbee.readAttribute(0x0402,0x0000,[:],200) + //temp 188 | zigbee.readAttribute(0x0405,0x0000,[:],200) + //humidity 189 | zigbee.readAttribute(0x0403,0x0000,[:],200) + //pressure 190 | zigbee.readAttribute(0x0400,0x0000,[:],200) //illuminance 191 | diagAttributes.each{ it -> 192 | //log.debug "it:${it.value.val}" 193 | cmds += zigbee.readAttribute(0x0B05,it.value.val,[:],200) 194 | } 195 | return cmds 196 | } 197 | 198 | def configure() { 199 | log.debug "Configuring Reporting and Bindings." 200 | runIn(1800,logsOff) 201 | 202 | //temp offset init 203 | //state.tempOffset = 0 204 | 205 | List cmds = zigbee.temperatureConfig(5,300) //temp 206 | cmds = cmds + zigbee.configureReporting(0x0405, 0x0000, DataType.UINT16, 5, 300, 100) //humidity 207 | cmds = cmds + zigbee.configureReporting(0x0403, 0x0000, DataType.UINT16, 5, 300, 2) //pressure 208 | cmds = cmds + zigbee.configureReporting(0x0400, 0x0000, DataType.UINT16, 1, 300, 500) //illuminance 209 | cmds = cmds + refresh() 210 | log.info "cmds:${cmds}" 211 | return cmds 212 | } 213 | 214 | def updated() { 215 | log.trace "Updated()" 216 | log.warn "debug logging is: ${logEnable == true}" 217 | log.warn "description logging is: ${txtEnable == true}" 218 | if (logEnable) runIn(1800,logsOff) 219 | 220 | /* example temp offset 221 | def crntTemp = device?.currentValue("temperature") 222 | if (refTemp && crntTemp && state.sensorTemp) { 223 | def prevOffset = (state.tempOffset ?: 0).toFloat().round(2) 224 | def deviceTemp = state.sensorTemp.toFloat().round(2) 225 | def newOffset = (refTemp.toFloat() - deviceTemp).round(2) 226 | def newTemp = (deviceTemp + newOffset).round(2) 227 | //send new event on offSet change 228 | if (newOffset.toString() != prevOffset.toString()){ 229 | state.tempOffset = newOffset 230 | def map = [name: "temperature", value: "${newTemp}", descriptionText: "${device.displayName} temperature offset was set to ${newOffset}°${location.temperatureScale}"] 231 | if (txtEnable) log.info "${map.descriptionText}" 232 | sendEvent(map) 233 | } 234 | //clear refTemp so it doesn't get changed later... 235 | device.removeSetting("refTemp") 236 | } 237 | */ 238 | } 239 | -------------------------------------------------------------------------------- /examples/drivers/virtualLock.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 -> 2020 Hubitat Inc. All Rights Reserved 3 | 4 | virtual lock with lock codes for testing new lockCode capabilities 5 | 2020-03-11 2.2.0 maxwell 6 | -refactor 7 | -fix lock codes starting with 0 not working 8 | 2019-09-08 2.1.5 ravenel 9 | -add lastCodeName 10 | 2019-09-04 2.1.5 maxwell 11 | -add test code on initial install 12 | -force state change on lock code event 13 | 2018-10-29 maxwell 14 | -add getCodes stub 15 | 2018-07-08 maxwell 16 | -add encryption support 17 | -add extended comments for community 18 | 19 | */ 20 | import groovy.json.JsonSlurper 21 | import groovy.json.JsonOutput 22 | 23 | metadata { 24 | definition (name: "Virtual Lock", namespace: "hubitat", author: "Mike Maxwell") { 25 | capability "Actuator" 26 | capability "Lock" 27 | capability "Lock Codes" 28 | capability "Refresh" 29 | capability "Switch" 30 | 31 | command "testSetMaxCodes", ["NUMBER"] 32 | command "testUnlockWithCode", ["STRING"] 33 | attribute "lastCodeName", "STRING" 34 | } 35 | 36 | preferences{ 37 | input name: "optEncrypt", type: "bool", title: "Enable lockCode encryption", defaultValue: false, description: "" 38 | //standard logging options for all drivers 39 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false, description: "" 40 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true, description: "" 41 | } 42 | } 43 | 44 | void logsOff(){ 45 | log.warn "debug logging disabled..." 46 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 47 | } 48 | 49 | void installed(){ 50 | log.warn "installed..." 51 | sendEvent(name:"maxCodes",value:20) 52 | sendEvent(name:"codeLength",value:4) 53 | //add a test lock code 54 | setCode(1, "1234", "Hubitat") 55 | lock() 56 | } 57 | 58 | void updated() { 59 | log.info "updated..." 60 | log.warn "description logging is: ${txtEnable == true}" 61 | log.warn "encryption is: ${optEncrypt == true}" 62 | //check crnt lockCodes for encryption status 63 | updateEncryption() 64 | //turn off debug logs after 30 minutes 65 | if (logEnable) runIn(1800,logsOff) 66 | } 67 | 68 | //handler for hub to hub integrations 69 | void parse(String description) { 70 | if (logEnable) log.debug "parse ${description}" 71 | if (description == "locked") lock() 72 | else if (description == "unlocked") unlock() 73 | } 74 | 75 | //capability commands 76 | void refresh() { 77 | sendEvent(name:"lock", value: device.currentValue("lock")) 78 | } 79 | 80 | void lock(){ 81 | String descriptionText = "${device.displayName} was locked" 82 | if (txtEnable) log.info "${descriptionText}" 83 | sendEvent(name:"lock",value:"locked",descriptionText: descriptionText, type:"digital") 84 | } 85 | 86 | void on() { 87 | lock() 88 | } 89 | 90 | void off() { 91 | unlock() 92 | } 93 | 94 | void unlock(){ 95 | /* 96 | on sucess event 97 | name value data 98 | lock unlocked | unlocked with timeout [:[code:, name:]] 99 | */ 100 | String descriptionText = "${device.displayName} was unlocked [digital]" 101 | if (txtEnable) log.info "${descriptionText}" 102 | sendEvent(name:"lock",value:"unlocked",descriptionText: descriptionText, type:"digital") 103 | } 104 | 105 | void setCodeLength(length){ 106 | /* 107 | on install/configure/change 108 | name value 109 | codeLength length 110 | */ 111 | String descriptionText = "${device.displayName} codeLength set to ${length}" 112 | if (txtEnable) log.info "${descriptionText}" 113 | sendEvent(name:"codeLength",value:length,descriptionText:descriptionText) 114 | } 115 | 116 | void setCode(codeNumber, code, name = null) { 117 | /* 118 | on sucess 119 | name value data notes 120 | codeChanged added | changed [":["code":"", "name":""]] default name to code # 121 | lockCodes JSON map of all lockCode 122 | */ 123 | if (codeNumber == null || codeNumber == 0 || code == null) return 124 | 125 | if (logEnable) log.debug "setCode- ${codeNumber}" 126 | 127 | if (!name) name = "code #${codeNumber}" 128 | 129 | Map lockCodes = getLockCodes() 130 | Map codeMap = getCodeMap(lockCodes,codeNumber) 131 | if (!changeIsValid(lockCodes,codeMap,codeNumber,code,name)) return 132 | 133 | Map data = [:] 134 | String value 135 | 136 | if (logEnable) log.debug "setting code ${codeNumber} to ${code} for lock code name ${name}" 137 | 138 | if (codeMap) { 139 | if (codeMap.name != name || codeMap.code != code) { 140 | codeMap = ["name":"${name}", "code":"${code}"] 141 | lockCodes."${codeNumber}" = codeMap 142 | data = ["${codeNumber}":codeMap] 143 | if (optEncrypt) data = encrypt(JsonOutput.toJson(data)) 144 | value = "changed" 145 | } 146 | } else { 147 | codeMap = ["name":"${name}", "code":"${code}"] 148 | data = ["${codeNumber}":codeMap] 149 | lockCodes << data 150 | if (optEncrypt) data = encrypt(JsonOutput.toJson(data)) 151 | value = "added" 152 | } 153 | updateLockCodes(lockCodes) 154 | sendEvent(name:"codeChanged",value:value,data:data, isStateChange: true) 155 | } 156 | 157 | void deleteCode(codeNumber) { 158 | /* 159 | on sucess 160 | name value data 161 | codeChanged deleted [":["code":"", "name":""]] 162 | lockCodes [":["code":"", "name":""],":["code":"", "name":""]] 163 | */ 164 | Map codeMap = getCodeMap(lockCodes,"${codeNumber}") 165 | if (codeMap) { 166 | Map result = [:] 167 | //build new lockCode map, exclude deleted code 168 | lockCodes.each{ 169 | if (it.key != "${codeNumber}"){ 170 | result << it 171 | } 172 | } 173 | updateLockCodes(result) 174 | Map data = ["${codeNumber}":codeMap] 175 | //encrypt lockCode data is requested 176 | if (optEncrypt) data = encrypt(JsonOutput.toJson(data)) 177 | sendEvent(name:"codeChanged",value:"deleted",data:data, isStateChange: true) 178 | } 179 | } 180 | 181 | //virtual test methods 182 | void testSetMaxCodes(length){ 183 | //on a real lock this event is generated from the response to a configuration report request 184 | sendEvent(name:"maxCodes",value:length) 185 | } 186 | 187 | void testUnlockWithCode(code = null){ 188 | if (logEnable) log.debug "testUnlockWithCode: ${code}" 189 | /* 190 | lockCodes in this context calls the helper function getLockCodes() 191 | */ 192 | Object lockCode = lockCodes.find{ it.value.code == "${code}" } 193 | if (lockCode){ 194 | Map data = ["${lockCode.key}":lockCode.value] 195 | String descriptionText = "${device.displayName} was unlocked by ${lockCode.value.name}" 196 | if (txtEnable) log.info "${descriptionText}" 197 | if (optEncrypt) data = encrypt(JsonOutput.toJson(data)) 198 | sendEvent(name:"lock",value:"unlocked",descriptionText: descriptionText, type:"physical",data:data, isStateChange: true) 199 | sendEvent(name:"lastCodeName", value: lockCode.value.name, descriptionText: descriptionText, isStateChange: true) 200 | } else { 201 | if (txtEnable) log.debug "testUnlockWithCode failed with invalid code" 202 | } 203 | } 204 | 205 | //helpers 206 | Boolean changeIsValid(lockCodes,codeMap,codeNumber,code,name){ 207 | //validate proposed lockCode change 208 | Boolean result = true 209 | Integer maxCodeLength = device.currentValue("codeLength")?.toInteger() ?: 4 210 | Integer maxCodes = device.currentValue("maxCodes")?.toInteger() ?: 20 211 | Boolean isBadLength = code.size() > maxCodeLength 212 | Boolean isBadCodeNum = maxCodes < codeNumber 213 | if (lockCodes) { 214 | List nameSet = lockCodes.collect{ it.value.name } 215 | List codeSet = lockCodes.collect{ it.value.code } 216 | if (codeMap) { 217 | nameSet = nameSet.findAll{ it != codeMap.name } 218 | codeSet = codeSet.findAll{ it != codeMap.code } 219 | } 220 | Boolean nameInUse = name in nameSet 221 | Boolean codeInUse = code in codeSet 222 | if (nameInUse || codeInUse) { 223 | if (nameInUse) { log.warn "changeIsValid:false, name:${name} is in use:${ lockCodes.find{ it.value.name == "${name}" } }" } 224 | if (codeInUse) { log.warn "changeIsValid:false, code:${code} is in use:${ lockCodes.find{ it.value.code == "${code}" } }" } 225 | result = false 226 | } 227 | } 228 | if (isBadLength || isBadCodeNum) { 229 | if (isBadLength) { log.warn "changeIsValid:false, length of code ${code} does not match codeLength of ${maxCodeLength}" } 230 | if (isBadCodeNum) { log.warn "changeIsValid:false, codeNumber ${codeNumber} is larger than maxCodes of ${maxCodes}" } 231 | result = false 232 | } 233 | return result 234 | } 235 | 236 | Map getCodeMap(lockCodes,codeNumber){ 237 | Map codeMap = [:] 238 | Map lockCode = lockCodes?."${codeNumber}" 239 | if (lockCode) { 240 | codeMap = ["name":"${lockCode.name}", "code":"${lockCode.code}"] 241 | } 242 | return codeMap 243 | } 244 | 245 | Map getLockCodes() { 246 | /* 247 | on a real lock we would fetch these from the response to a userCode report request 248 | */ 249 | String lockCodes = device.currentValue("lockCodes") 250 | Map result = [:] 251 | if (lockCodes) { 252 | //decrypt codes if they're encrypted 253 | if (lockCodes[0] == "{") result = new JsonSlurper().parseText(lockCodes) 254 | else result = new JsonSlurper().parseText(decrypt(lockCodes)) 255 | } 256 | return result 257 | } 258 | 259 | void getCodes() { 260 | //no op 261 | } 262 | 263 | void updateLockCodes(lockCodes){ 264 | /* 265 | whenever a code changes we update the lockCodes event 266 | */ 267 | if (logEnable) log.debug "updateLockCodes: ${lockCodes}" 268 | String strCodes = JsonOutput.toJson(lockCodes) 269 | if (optEncrypt) { 270 | strCodes = encrypt(strCodes) 271 | } 272 | sendEvent(name:"lockCodes", value:strCodes, isStateChange:true) 273 | } 274 | 275 | void updateEncryption(){ 276 | /* 277 | resend lockCodes map when the encryption option is changed 278 | */ 279 | String lockCodes = device.currentValue("lockCodes") //encrypted or decrypted 280 | if (lockCodes){ 281 | if (optEncrypt && lockCodes[0] == "{") { //resend encrypted 282 | sendEvent(name:"lockCodes",value: encrypt(lockCodes)) 283 | } else if (!optEncrypt && lockCodes[0] != "{") { //resend decrypted 284 | sendEvent(name:"lockCodes",value: decrypt(lockCodes)) 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /examples/drivers/virtualThermostat.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Virtual Thermostat 3 | 4 | Copyright 2016 -> 2023 Hubitat Inc. All Rights Reserved 5 | 6 | */ 7 | 8 | metadata { 9 | definition ( 10 | name: "Virtual Thermostat", 11 | namespace: "hubitat", 12 | author: "Kevin L., Mike M., Bruce R." 13 | ) { 14 | capability "Actuator" 15 | capability "Sensor" 16 | capability "Temperature Measurement" 17 | capability "Thermostat" 18 | 19 | attribute "supportedThermostatFanModes", "JSON_OBJECT" 20 | attribute "supportedThermostatModes", "JSON_OBJECT" 21 | attribute "hysteresis", "NUMBER" 22 | 23 | // Commands needed to change internal attributes of virtual device. 24 | command "setTemperature", ["NUMBER"] 25 | command "setThermostatOperatingState", ["ENUM"] 26 | command "setThermostatSetpoint", ["NUMBER"] 27 | command "setSupportedThermostatFanModes", ["JSON_OBJECT"] 28 | command "setSupportedThermostatModes", ["JSON_OBJECT"] 29 | } 30 | 31 | preferences { 32 | input( name: "hysteresis",type:"enum",title: "Thermostat hysteresis degrees", options:["0.1","0.25","0.5","1","2"], description:"", defaultValue: 0.5) 33 | input( name: "logEnable", type:"bool", title: "Enable debug logging",defaultValue: false) 34 | input( name: "txtEnable", type:"bool", title: "Enable descriptionText logging", defaultValue: true) 35 | } 36 | } 37 | import groovy.json.JsonOutput 38 | 39 | def installed() { 40 | log.warn "installed..." 41 | initialize() 42 | } 43 | 44 | def updated() { 45 | log.info "updated..." 46 | log.warn "debug logging is: ${logEnable == true}" 47 | log.warn "description logging is: ${txtEnable == true}" 48 | if (logEnable) runIn(1800,logsOff) 49 | initialize() 50 | } 51 | 52 | def initialize() { 53 | if (state?.lastRunningMode == null) { 54 | sendEvent(name: "temperature", value: convertTemperatureIfNeeded(68.0,"F",1)) 55 | sendEvent(name: "thermostatSetpoint", value: convertTemperatureIfNeeded(68.0,"F",1)) 56 | sendEvent(name: "heatingSetpoint", value: convertTemperatureIfNeeded(68.0,"F",1)) 57 | sendEvent(name: "coolingSetpoint", value: convertTemperatureIfNeeded(75.0,"F",1)) 58 | state.lastRunningMode = "heat" 59 | updateDataValue("lastRunningMode", "heat") 60 | setThermostatOperatingState("idle") 61 | setSupportedThermostatFanModes(JsonOutput.toJson(["auto","circulate","on"])) 62 | setSupportedThermostatModes(JsonOutput.toJson(["auto", "cool", "emergency heat", "heat", "off"])) 63 | off() 64 | fanAuto() 65 | } 66 | sendEvent(name: "hysteresis", value: (hysteresis ?: 0.5).toBigDecimal()) 67 | } 68 | 69 | def logsOff(){ 70 | log.warn "debug logging disabled..." 71 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 72 | } 73 | 74 | def manageCycle(){ 75 | def ambientTempChangePerCycle = 0.25 76 | def hvacTempChangePerCycle = 0.75 77 | 78 | def hysteresis = (hysteresis ?: 0.5).toBigDecimal() 79 | 80 | def coolingSetpoint = (device.currentValue("coolingSetpoint") ?: convertTemperatureIfNeeded(75.0,"F",1)).toBigDecimal() 81 | def heatingSetpoint = (device.currentValue("heatingSetpoint") ?: convertTemperatureIfNeeded(68.0,"F",1)).toBigDecimal() 82 | def temperature = (device.currentValue("temperature") ?: convertTemperatureIfNeeded(68.0,"F",1)).toBigDecimal() 83 | 84 | def thermostatMode = device.currentValue("thermostatMode") ?: "off" 85 | def thermostatOperatingState = device.currentValue("thermostatOperatingState") ?: "idle" 86 | 87 | def ambientGain = (temperature + ambientTempChangePerCycle).setScale(2) 88 | def ambientLoss = (temperature - ambientTempChangePerCycle).setScale(2) 89 | def coolLoss = (temperature - hvacTempChangePerCycle).setScale(2) 90 | def heatGain = (temperature + hvacTempChangePerCycle).setScale(2) 91 | 92 | def coolingOn = (temperature >= (coolingSetpoint + hysteresis)) 93 | if (thermostatOperatingState == "cooling") coolingOn = temperature >= (coolingSetpoint - hysteresis) 94 | 95 | def heatingOn = (temperature <= (heatingSetpoint - hysteresis)) 96 | if (thermostatOperatingState == "heating") heatingOn = (temperature <= (heatingSetpoint + hysteresis)) 97 | 98 | if (thermostatMode == "cool") { 99 | if (coolingOn && thermostatOperatingState != "cooling") setThermostatOperatingState("cooling") 100 | else if (thermostatOperatingState != "idle") setThermostatOperatingState("idle") 101 | } else if (thermostatMode == "heat") { 102 | if (heatingOn && thermostatOperatingState != "heating") setThermostatOperatingState("heating") 103 | else if (thermostatOperatingState != "idle") setThermostatOperatingState("idle") 104 | } else if (thermostatMode == "auto") { 105 | if (heatingOn && coolingOn) log.error "cooling and heating are on- temp:${temperature}" 106 | else if (coolingOn && thermostatOperatingState != "cooling") setThermostatOperatingState("cooling") 107 | else if (heatingOn && thermostatOperatingState != "heating") setThermostatOperatingState("heating") 108 | else if ((!coolingOn || !heatingOn) && thermostatOperatingState != "idle") setThermostatOperatingState("idle") 109 | } 110 | } 111 | 112 | // Commands needed to change internal attributes of virtual device. 113 | def setTemperature(temperature) { 114 | logDebug "setTemperature(${temperature}) was called" 115 | sendTemperatureEvent("temperature", temperature) 116 | runIn(1, manageCycle) 117 | } 118 | 119 | def setHumidity(humidity) { 120 | logDebug "setHumidity(${humidity}) was called" 121 | sendEvent(name: "humidity", value: humidity, unit: "%", descriptionText: getDescriptionText("humidity set to ${humidity}%")) 122 | } 123 | 124 | def setThermostatOperatingState (operatingState) { 125 | logDebug "setThermostatOperatingState (${operatingState}) was called" 126 | updateSetpoints(null,null,null,operatingState) 127 | sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: getDescriptionText("thermostatOperatingState set to ${operatingState}")) 128 | } 129 | 130 | def setSupportedThermostatFanModes(fanModes) { 131 | logDebug "setSupportedThermostatFanModes(${fanModes}) was called" 132 | // (auto, circulate, on) 133 | sendEvent(name: "supportedThermostatFanModes", value: fanModes, descriptionText: getDescriptionText("supportedThermostatFanModes set to ${fanModes}")) 134 | } 135 | 136 | def setSupportedThermostatModes(modes) { 137 | logDebug "setSupportedThermostatModes(${modes}) was called" 138 | // (auto, cool, emergency heat, heat, off) 139 | sendEvent(name: "supportedThermostatModes", value: modes, descriptionText: getDescriptionText("supportedThermostatModes set to ${modes}")) 140 | } 141 | 142 | 143 | def auto() { setThermostatMode("auto") } 144 | 145 | def cool() { setThermostatMode("cool") } 146 | 147 | def emergencyHeat() { setThermostatMode("heat") } 148 | 149 | def heat() { setThermostatMode("heat") } 150 | def off() { setThermostatMode("off") } 151 | 152 | def setThermostatMode(mode) { 153 | sendEvent(name: "thermostatMode", value: "${mode}", descriptionText: getDescriptionText("thermostatMode is ${mode}")) 154 | setThermostatOperatingState ("idle") 155 | updateSetpoints(null, null, null, mode) 156 | runIn(1, manageCycle) 157 | } 158 | 159 | def fanAuto() { setThermostatFanMode("auto") } 160 | def fanCirculate() { setThermostatFanMode("circulate") } 161 | def fanOn() { setThermostatFanMode("on") } 162 | 163 | def setThermostatFanMode(fanMode) { 164 | sendEvent(name: "thermostatFanMode", value: "${fanMode}", descriptionText: getDescriptionText("thermostatFanMode is ${fanMode}")) 165 | } 166 | 167 | def setThermostatSetpoint(setpoint) { 168 | logDebug "setThermostatSetpoint(${setpoint}) was called" 169 | updateSetpoints(setpoint, null, null, null) 170 | } 171 | 172 | def setCoolingSetpoint(setpoint) { 173 | logDebug "setCoolingSetpoint(${setpoint}) was called" 174 | updateSetpoints(null, null, setpoint, null) 175 | } 176 | 177 | def setHeatingSetpoint(setpoint) { 178 | logDebug "setHeatingSetpoint(${setpoint}) was called" 179 | updateSetpoints(null, setpoint, null, null) 180 | } 181 | 182 | private updateSetpoints(sp = null, hsp = null, csp = null, operatingState = null){ 183 | if (operatingState in ["off"]) return 184 | if (hsp == null) hsp = device.currentValue("heatingSetpoint",true) 185 | if (csp == null) csp = device.currentValue("coolingSetpoint",true) 186 | if (sp == null) sp = device.currentValue("thermostatSetpoint",true) 187 | 188 | if (operatingState == null) operatingState = state.lastRunningMode 189 | 190 | def hspChange = isStateChange(device,"heatingSetpoint",hsp.toString()) 191 | def cspChange = isStateChange(device,"coolingSetpoint",csp.toString()) 192 | def spChange = isStateChange(device,"thermostatSetpoint",sp.toString()) 193 | def osChange = operatingState != state.lastRunningMode 194 | 195 | def newOS 196 | def descriptionText 197 | def name 198 | def value 199 | def unit = "°${location.temperatureScale}" 200 | switch (operatingState) { 201 | case ["pending heat","heating","heat"]: 202 | newOS = "heat" 203 | if (spChange) { 204 | hspChange = true 205 | hsp = sp 206 | } else if (hspChange || osChange) { 207 | spChange = true 208 | sp = hsp 209 | } 210 | if (csp - 2 < hsp) { 211 | csp = hsp + 2 212 | cspChange = true 213 | } 214 | break 215 | case ["pending cool","cooling","cool"]: 216 | newOS = "cool" 217 | if (spChange) { 218 | cspChange = true 219 | csp = sp 220 | } else if (cspChange || osChange) { 221 | spChange = true 222 | sp = csp 223 | } 224 | if (hsp + 2 > csp) { 225 | hsp = csp - 2 226 | hspChange = true 227 | } 228 | break 229 | default : 230 | return 231 | } 232 | 233 | if (hspChange) { 234 | value = hsp 235 | name = "heatingSetpoint" 236 | descriptionText = "${device.displayName} ${name} was set to ${value}${unit}" 237 | if (txtEnable) log.info descriptionText 238 | sendEvent(name: name, value: value, descriptionText: descriptionText, unit: unit, isStateChange: true) 239 | } 240 | if (cspChange) { 241 | value = csp 242 | name = "coolingSetpoint" 243 | descriptionText = "${device.displayName} ${name} was set to ${value}${unit}" 244 | if (txtEnable) log.info descriptionText 245 | sendEvent(name: name, value: value, descriptionText: descriptionText, unit: unit, isStateChange: true) 246 | } 247 | if (spChange) { 248 | value = sp 249 | name = "thermostatSetpoint" 250 | descriptionText = "${device.displayName} ${name} was set to ${value}${unit}" 251 | if (txtEnable) log.info descriptionText 252 | sendEvent(name: name, value: value, descriptionText: descriptionText, unit: unit, isStateChange: true) 253 | } 254 | 255 | state.lastRunningMode = newOS 256 | updateDataValue("lastRunningMode", newOS) 257 | } 258 | 259 | def setSchedule(schedule) { 260 | sendEvent(name: "schedule", value: "${schedule}", descriptionText: getDescriptionText("schedule is ${schedule}")) 261 | } 262 | 263 | private sendTemperatureEvent(name, val) { 264 | sendEvent(name: "${name}", value: val, unit: "°${getTemperatureScale()}", descriptionText: getDescriptionText("${name} is ${val} °${getTemperatureScale()}"), isStateChange: true) 265 | } 266 | 267 | 268 | def parse(String description) { 269 | logDebug "$description" 270 | } 271 | 272 | 273 | private logDebug(msg) { 274 | if (settings?.logEnable) log.debug "${msg}" 275 | } 276 | 277 | private getDescriptionText(msg) { 278 | def descriptionText = "${device.displayName} ${msg}" 279 | if (settings?.txtEnable) log.info "${descriptionText}" 280 | return descriptionText 281 | } 282 | -------------------------------------------------------------------------------- /examples/drivers/thirdRealityMatterNightLight.groovy: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Third Reality Matter Multi-Function Night Light 4 | 5 | Copyright 2023 Hubitat Inc. All Rights Reserved 6 | 7 | 2023-11-02 2.3.7 maxwell 8 | -initial pub 9 | 10 | */ 11 | 12 | import groovy.transform.Field 13 | 14 | //transitionTime options 15 | @Field static Map ttOpts = [ 16 | defaultValue: "1" 17 | ,defaultText: "1s" 18 | ,options:["0":"ASAP","1":"1s","2":"2s","5":"5s"] 19 | ] 20 | 21 | @Field static Map colorRGBName = [ 22 | 4:"Red" 23 | ,13:"Orange" 24 | ,21:"Yellow" 25 | ,29:"Chartreuse" 26 | ,38:"Green" 27 | ,46:"Spring" 28 | ,54:"Cyan" 29 | ,63:"Azure" 30 | ,71:"Blue" 31 | ,79:"Violet" 32 | ,88:"Magenta" 33 | ,96:"Rose" 34 | ,101:"Red" 35 | ] 36 | 37 | metadata { 38 | definition (name: "Third Reality Matter Multi-Function Night Light", namespace: "hubitat", author: "Mike Maxwell") { 39 | capability "Actuator" 40 | capability "Switch" 41 | capability "SwitchLevel" 42 | capability "Configuration" 43 | capability "Illuminance Measurement" 44 | capability "Color Control" 45 | capability "Light" 46 | capability "Motion Sensor" 47 | capability "Initialize" 48 | 49 | fingerprint endpointId:"01", inClusters:"0003,0004,0006,0008,001D,0300,0406", outClusters:"", model:"Smart Color Night Light", manufacturer:"ThirdReality", controllerType:"MAT" 50 | fingerprint endpointId:"01", inClusters:"0003,0004,0006,0008,001D,0300", outClusters:"", model:"Smart Color Night Light", manufacturer:"ThirdReality", controllerType:"MAT" 51 | 52 | } 53 | preferences { 54 | input(name:"transitionTime", type:"enum", title:"Level transition time (default:${ttOpts.defaultText})", options:ttOpts.options, defaultValue:ttOpts.defaultValue) 55 | input(name:"rgbTransitionTime", type:"enum", title:"RGB transition time (default:${ttOpts.defaultText})", options:ttOpts.options, defaultValue:ttOpts.defaultValue) 56 | input(name:"logEnable", type:"bool", title:"Enable debug logging", defaultValue:false) 57 | input(name:"txtEnable", type:"bool", title:"Enable descriptionText logging", defaultValue:true) 58 | } 59 | } 60 | 61 | //parsers 62 | void parse(String description) { 63 | Map descMap = matter.parseDescriptionAsMap(description) 64 | if (logEnable) log.debug "descMap:${descMap}" 65 | switch (descMap.cluster) { 66 | case "0006" : 67 | if (descMap.attrId == "0000") { //switch 68 | sendSwitchEvent(descMap.value) 69 | } 70 | break 71 | case "0008" : 72 | if (descMap.attrId == "0000") { //current level 73 | sendLevelEvent(descMap.value) 74 | } 75 | break 76 | case "0000" : 77 | if (descMap.attrId == "4000") { //software build 78 | updateDataValue("softwareBuild",descMap.value ?: "unknown") 79 | } 80 | break 81 | case "0300" : 82 | if (descMap.attrId == "0000") { //hue 83 | sendHueEvent(descMap.value) 84 | } else if (descMap.attrId == "0001") { //saturation 85 | sendSaturationEvent(descMap.value) 86 | } //else log.trace "skipped color, attribute:${it.attrId}, value:${it.value}" 87 | break 88 | case "0400": 89 | if (descMap.attrId == "0000") { 90 | sendIlluminanceEvent(hexStrToUnsignedInt(descMap.value)) 91 | } 92 | break 93 | case "0406" : 94 | if (descMap.attrId == "0000") { 95 | sendMotionEvent((descMap.value == "00") ? "inactive" : "active") 96 | } 97 | break 98 | default : 99 | if (logEnable) { 100 | log.debug "skipped:${descMap}" 101 | } 102 | } 103 | } 104 | 105 | //events 106 | private void sendSwitchEvent(String rawValue) { 107 | String value = rawValue == "01" ? "on" : "off" 108 | if (device.currentValue("switch") == value) return 109 | String descriptionText = "${device.displayName} was turned ${value}" 110 | if (txtEnable) log.info descriptionText 111 | sendEvent(name:"switch", value:value, descriptionText:descriptionText) 112 | } 113 | 114 | private void sendLevelEvent(String rawValue) { 115 | Integer value = Math.round(hexStrToUnsignedInt(rawValue) / 2.55) 116 | if (value == 0 || value == device.currentValue("level")) return 117 | String descriptionText = "${device.displayName} level was set to ${value}%" 118 | if (txtEnable) log.info descriptionText 119 | sendEvent(name:"level", value:value, descriptionText:descriptionText, unit: "%") 120 | } 121 | 122 | private void sendHueEvent(String rawValue, Boolean presetColor = false) { 123 | Integer value = hex254ToInt100(rawValue) 124 | sendRGBNameEvent(value) 125 | String descriptionText = "${device.displayName} hue was set to ${value}%" 126 | if (txtEnable) log.info descriptionText 127 | sendEvent(name:"hue", value:value, descriptionText:descriptionText, unit: "%") 128 | } 129 | 130 | private void sendSaturationEvent(String rawValue, Boolean presetColor = false) { 131 | Integer value = hex254ToInt100(rawValue) 132 | sendRGBNameEvent(null,value) 133 | String descriptionText = "${device.displayName} saturation was set to ${value}%" 134 | if (txtEnable) log.info descriptionText 135 | sendEvent(name:"saturation", value:value, descriptionText:descriptionText, unit: "%") 136 | } 137 | 138 | private void sendRGBNameEvent(hue, sat = null){ 139 | String genericName 140 | if (device.currentValue("saturation") == 0) { 141 | genericName = "White" 142 | } else if (hue == null) { 143 | return 144 | } else { 145 | genericName = colorRGBName.find{k , v -> hue < k}.value 146 | } 147 | if (genericName == device.currentValue("colorName")) return 148 | String descriptionText = "${device.displayName} color is ${genericName}" 149 | if (txtEnable) log.info descriptionText 150 | sendEvent(name: "colorName", value: genericName ,descriptionText: descriptionText) 151 | } 152 | 153 | void sendMotionEvent(value) { 154 | if (device.currentValue("motion") == value) return 155 | String descriptionText = "${device.displayName} is ${value}" 156 | if (txtEnable) log.info descriptionText 157 | sendEvent(name: "motion",value: value,descriptionText: descriptionText) 158 | } 159 | 160 | void sendIlluminanceEvent(rawValue) { 161 | Integer value = getLuxValue(rawValue) 162 | Integer pv = device.currentValue("illuminance") ?: 0 163 | if (pv == value) return 164 | String descriptionText = "${device.displayName} illuminance is ${value} Lux" 165 | if (txtEnable) log.info descriptionText 166 | sendEvent(name: "illuminance",value: value,descriptionText: descriptionText,unit: "Lux") 167 | } 168 | 169 | //capability commands 170 | void on() { 171 | if (logEnable) log.debug "on()" 172 | sendToDevice(matter.on()) 173 | } 174 | 175 | void off() { 176 | if (logEnable) log.debug "off()" 177 | sendToDevice(matter.off()) 178 | } 179 | 180 | void setLevel(Object value) { 181 | if (logEnable) "setLevel(${value})" 182 | setLevel(value,transitionTime ?: 1) 183 | } 184 | 185 | void setLevel(Object value, Object rate) { 186 | if (logEnable) log.debug "setLevel(${value}, ${rate})" 187 | Integer level = value.toInteger() 188 | if (level == 0 && device.currentValue("switch") == "off") return 189 | sendToDevice(matter.setLevel(level, rate.toInteger())) 190 | } 191 | 192 | void setHue(Object value) { 193 | if (logEnable) log.debug "setHue(${value})" 194 | List cmds = [] 195 | Integer transitionTime = ( rgbTransitionTime ?: 1).toInteger() 196 | if (device.currentValue("switch") == "on"){ 197 | cmds.add(matter.setHue(value.toInteger(), transitionTime)) 198 | } else { 199 | cmds.add(matter.on()) 200 | cmds.add(matter.setHue(value.toInteger(), transitionTime)) 201 | } 202 | sendToDevice(cmds) 203 | } 204 | 205 | void setSaturation(Object value) { 206 | if (logEnable) log.debug "setSaturation(${value})" 207 | List cmds = [] 208 | Integer transitionTime = ( rgbTransitionTime ?: 1).toInteger() 209 | if (device.currentValue("switch") == "on"){ 210 | cmds.add(matter.setSaturation(value.toInteger(), transitionTime)) 211 | } else { 212 | cmds.add(matter.on()) 213 | cmds.add(matter.setSaturation(value.toInteger(), transitionTime)) 214 | } 215 | sendToDevice(cmds) 216 | } 217 | 218 | void setHueSat(Object hue, Object sat) { 219 | if (logEnable) log.debug "setHueSat(${hue}, ${sat})" 220 | List cmds = [] 221 | Integer transitionTime = ( rgbTransitionTime ?: 1).toInteger() 222 | if (device.currentValue("switch") == "on"){ 223 | cmds.add(matter.setHue(hue.toInteger(), transitionTime)) 224 | cmds.add(matter.setSaturation(sat.toInteger(), transitionTime)) 225 | } else { 226 | cmds.add(matter.on()) 227 | cmds.add(matter.setHue(hue.toInteger(), transitionTime)) 228 | cmds.add(matter.setSaturation(sat.toInteger(), transitionTime)) 229 | } 230 | sendToDevice(cmds) 231 | } 232 | 233 | void setColor(Map colorMap) { 234 | if (logEnable) log.debug "setColor(${colorMap})" 235 | if (colorMap.level) { 236 | setLevel(colorMap.level) 237 | } 238 | if (colorMap.hue != null && colorMap.saturation != null) { 239 | setHueSat(colorMap.hue, colorMap.saturation) 240 | } else if (colorMap.hue != null) { 241 | setHue(colorMap.hue) 242 | } else if (colorMap.saturation != null) { 243 | setSaturation(colorMap.saturation) 244 | } 245 | } 246 | 247 | void configure() { 248 | log.warn "configure..." 249 | sendToDevice(subscribeCmd()) 250 | } 251 | 252 | //lifecycle commands 253 | void updated(){ 254 | log.info "updated..." 255 | log.warn "debug logging is: ${logEnable == true}" 256 | log.warn "description logging is: ${txtEnable == true}" 257 | if (logEnable) runIn(1800,logsOff) 258 | } 259 | 260 | void initialize() { 261 | sendToDevice(subscribeCmd()) 262 | } 263 | 264 | void refresh() { 265 | if (logEnable) log.debug "refresh()" 266 | sendToDevice(refreshCmd()) 267 | } 268 | 269 | String refreshCmd() { 270 | List> attributePaths = [] 271 | attributePaths.add(matter.attributePath(device.endpointId, 0x0006, 0x0000)) 272 | attributePaths.add(matter.attributePath(device.endpointId, 0x0008, 0x0000)) 273 | attributePaths.add(matter.attributePath(device.endpointId, 0x0300, 0x0000)) 274 | attributePaths.add(matter.attributePath(device.endpointId, 0x0300, 0x0001)) 275 | attributePaths.add(matter.attributePath(device.endpointId, 0x0300, 0x0007)) 276 | attributePaths.add(matter.attributePath(device.endpointId, 0x0300, 0x0008)) 277 | 278 | attributePaths.add(matter.attributePath(0x02, 0x0400, 0x0000)) //illuminance 279 | attributePaths.add(matter.attributePath(0x03, 0x0406, 0x0000)) //occupancy 280 | 281 | String cmd = matter.readAttributes(attributePaths) 282 | return cmd 283 | } 284 | 285 | String subscribeCmd() { 286 | List> attributePaths = [] 287 | attributePaths.add(matter.attributePath(0x01, 0x0006, 0x00)) 288 | attributePaths.add(matter.attributePath(0x01, 0x0008, 0x00)) 289 | attributePaths.add(matter.attributePath(0x01, 0x0300, 0x00)) 290 | attributePaths.add(matter.attributePath(0x01, 0x0300, 0x01)) 291 | attributePaths.add(matter.attributePath(0x01, 0x0300, 0x07)) 292 | attributePaths.add(matter.attributePath(0x01, 0x0300, 0x08)) 293 | 294 | 295 | attributePaths.add(matter.attributePath(0x02, 0x0400, 0x0000)) //illuminance 296 | attributePaths.add(matter.attributePath(0x03, 0x0406, 0x0000)) //occupancy 297 | //standard 0 reporting interval is way too busy for bulbs 298 | String cmd = matter.subscribe(5,0xFFFF,attributePaths) 299 | return cmd 300 | } 301 | 302 | void logsOff(){ 303 | log.warn "debug logging disabled..." 304 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 305 | } 306 | 307 | Integer hex254ToInt100(String value) { 308 | return Math.round(hexStrToUnsignedInt(value) / 2.54) 309 | } 310 | 311 | String int100ToHex254(value) { 312 | return intToHexStr(Math.round(value * 2.54)) 313 | } 314 | 315 | Integer getLuxValue(rawValue) { 316 | return Math.max((Math.pow(10,(rawValue/10000)) - 1).toInteger(),1) 317 | } 318 | 319 | void sendToDevice(List cmds, Integer delay = 300) { 320 | sendHubCommand(new hubitat.device.HubMultiAction(commands(cmds, delay), hubitat.device.Protocol.MATTER)) 321 | } 322 | 323 | void sendToDevice(String cmd, Integer delay = 300) { 324 | sendHubCommand(new hubitat.device.HubAction(cmd, hubitat.device.Protocol.MATTER)) 325 | } 326 | 327 | List commands(List cmds, Integer delay = 300) { 328 | return delayBetween(cmds.collect { it }, delay) 329 | } 330 | 331 | -------------------------------------------------------------------------------- /examples/drivers/genericZWaveCentralSceneDimmer.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Generic Z-Wave CentralScene Dimmer 3 | 4 | Copyright 2016 -> 2020 Hubitat Inc. All Rights Reserved 5 | 2020--07-31 2.2.3 maxwell 6 | -switch to internal secure encap method 7 | 2020-06-01 2.2.1 bcopeland 8 | -basicSet to switchMultilevelSet conversion 9 | 2020-03-25 2.2.0 maxwell 10 | -C7/S2 updates 11 | -refactor 12 | -remove DZMX1 (move to generic dimmer) 13 | -add supervision report response 14 | 2019-11-14 2.1.7 maxwell 15 | -add safe nav on flashrate 16 | -add command class versions 17 | -move some leviton dimmers over from generic 18 | 2018-07-15 maxwell 19 | -add all button commands 20 | 2018-06-04 maxwell 21 | -updates to support changeLevel 22 | 2018-03-26 maxwell 23 | -add standard level events algorithm 24 | 2018-03-24 maxwell 25 | -initial pub 26 | 27 | */ 28 | import groovy.transform.Field 29 | 30 | @Field static Map commandClassVersions = [ 31 | 0x20: 1 //basic 32 | ,0x26: 1 //switchMultiLevel 33 | ,0x5B: 1 //centralScene 34 | ] 35 | @Field static Map switchVerbs = [0:"was turned",1:"is"] 36 | @Field static Map levelVerbs = [0:"was set to",1:"is"] 37 | @Field static Map switchValues = [0:"off",1:"on"] 38 | 39 | metadata { 40 | definition (name: "Generic Z-Wave CentralScene Dimmer",namespace: "hubitat", author: "Mike Maxwell") { 41 | capability "Actuator" 42 | capability "Switch" 43 | capability "Switch Level" 44 | capability "ChangeLevel" 45 | capability "Configuration" 46 | capability "PushableButton" 47 | capability "HoldableButton" 48 | capability "ReleasableButton" 49 | capability "DoubleTapableButton" 50 | 51 | command "flash" 52 | command "refresh" 53 | command "push", ["NUMBER"] 54 | command "hold", ["NUMBER"] 55 | command "release", ["NUMBER"] 56 | command "doubleTap", ["NUMBER"] 57 | 58 | fingerprint deviceId: "3034", inClusters: "0x5E,0x86,0x72,0x5A,0x85,0x59,0x73,0x26,0x27,0x70,0x2C,0x2B,0x5B,0x7A", outClusters: "0x5B", mfr: "0315", prod: "4447", deviceJoinName: "ZWP WD-100 Dimmer" 59 | fingerprint deviceId: "3034", inClusters: "0x5E,0x86,0x72,0x5A,0x85,0x59,0x55,0x73,0x26,0x70,0x2C,0x2B,0x5B,0x7A,0x9F,0x6C", outClusters: "0x5B", mfr: "0315", prod: "4447", deviceJoinName: "ZLINK ZL-WD-100" 60 | fingerprint deviceId: "0209", inClusters: "0x26,0x27,0x2B,0x2C,0x85,0x72,0x86,0x91,0x77,0x73", outClusters: "0x82", mfr: "001D", prod: "0401", deviceJoinName: "Leviton VRI06-1LZ" 61 | fingerprint deviceId: "0209", inClusters: "0x25,0x27,0x2B,0x2C,0x85,0x72,0x86,0x91,0x77,0x73", outClusters: "0x82", mfr: "001D", prod: "0301", deviceJoinName: "Leviton VRI10-1LZ" 62 | fingerprint deviceId: "0209", inClusters: "0x26,0x27,0x2B,0x2C,0x85,0x72,0x86,0x91,0x77,0x73", outClusters: "0x82", mfr: "001D", prod: "0501", deviceJoinName: "Leviton ???" 63 | fingerprint deviceId: "0334", inClusters: "0x26,0x27,0x2B,0x2C,0x85,0x72,0x86,0x91,0x77,0x73", outClusters: "0x82", mfr: "001D", prod: "0602", deviceJoinName: "Leviton ???" 64 | fingerprint deviceId: "0001", inClusters: "0x5E,0x85,0x59,0x86,0x72,0x70,0x5A,0x73,0x26,0x20,0x27,0x2C,0x2B,0x7A", outClusters: "0x82", mfr: "001D", prod: "3501", deviceJoinName: "Leviton DZPD3-2BW" 65 | fingerprint deviceId: "3034", inClusters: "0x5E,0x55,0x9F", outClusters: "0x5B", mfr: "000C", prod: "4447", deviceJoinName: "Homeseer HS-WD100+" 66 | fingerprint deviceId: "3034", inClusters: "0x5E,0x86,0x72,0x5A,0x85,0x59,0x55,0x73,0x26,0x70,0x2C,0x2B,0x5B,0x7A,0x9F,0x6C", outClusters: "0x5B", mfr: "000C", prod: "4447", deviceJoinName: "Homeseer HS-WD100+" 67 | } 68 | 69 | preferences { 70 | input name: "param4", type: "enum", title: "Paddle function", options:[[0:"Normal"],[1:"Reverse"]], defaultValue: 0 71 | input name: "flashRate", type: "enum", title: "Flash rate", options:[[750:"750ms"],[1000:"1s"],[2000:"2s"],[5000:"5s"]], defaultValue: 750 72 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true 73 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 74 | } 75 | } 76 | 77 | void logsOff(){ 78 | log.warn "debug logging disabled..." 79 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 80 | } 81 | 82 | void parse(String description){ 83 | if (logEnable) log.debug "parse description: ${description}" 84 | hubitat.zwave.Command cmd = zwave.parse(description,commandClassVersions) 85 | if (cmd) { 86 | zwaveEvent(cmd) 87 | } 88 | } 89 | 90 | String secure(String cmd){ 91 | return zwaveSecureEncap(cmd) 92 | } 93 | 94 | String secure(hubitat.zwave.Command cmd){ 95 | return zwaveSecureEncap(cmd) 96 | } 97 | 98 | void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd){ 99 | hubitat.zwave.Command encapCmd = cmd.encapsulatedCommand(commandClassVersions) 100 | if (encapCmd) { 101 | zwaveEvent(encapCmd) 102 | } 103 | sendHubCommand(new hubitat.device.HubAction(secure(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0)), hubitat.device.Protocol.ZWAVE)) 104 | } 105 | 106 | String startLevelChange(direction){ 107 | Integer upDown = direction == "down" ? 1 : 0 108 | return secure(zwave.switchMultilevelV1.switchMultilevelStartLevelChange(upDown: upDown, ignoreStartLevel: 1, startLevel: 0)) 109 | } 110 | 111 | List stopLevelChange(){ 112 | return [ 113 | secure(zwave.switchMultilevelV1.switchMultilevelStopLevelChange()) 114 | ,"delay 200" 115 | ,secure(zwave.basicV1.basicGet()) 116 | ] 117 | } 118 | 119 | //returns on physical 120 | void zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd){ 121 | if (logEnable) log.debug "SwitchMultilevelReport value: ${cmd.value}" 122 | dimmerEvents(cmd.value,"physical") 123 | } 124 | 125 | //returns on digital 126 | void zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd){ 127 | if (logEnable) log.info "BasicReport value: ${cmd.value}" 128 | dimmerEvents(cmd.value,"digital") 129 | } 130 | 131 | void zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd){ 132 | if (logEnable) log.debug "CentralSceneNotification: ${cmd}" 133 | 134 | Integer button = cmd.sceneNumber 135 | Integer key = cmd.keyAttributes 136 | String action 137 | switch (key){ 138 | case 0: //pushed 139 | action = "pushed" 140 | break 141 | case 1: //released, only after 2 142 | state."${button}" = 0 143 | action = "released" 144 | break 145 | case 2: //holding 146 | if (state."${button}" == 0){ 147 | state."${button}" = 1 148 | runInMillis(200,delayHold,[data:button]) 149 | } 150 | break 151 | case 3: //double tap, 4 is tripple tap 152 | action = "doubleTapped" 153 | break 154 | } 155 | 156 | if (action){ 157 | sendButtonEvent(action, button, "physical") 158 | } 159 | } 160 | 161 | void zwaveEvent(hubitat.zwave.Command cmd){ 162 | if (logEnable) log.debug "skip: ${cmd}" 163 | } 164 | 165 | void dimmerEvents(rawValue,type){ 166 | if (logEnable) log.debug "dimmerEvents value: ${rawValue}, type: ${type}" 167 | Integer levelValue = rawValue.toInteger() 168 | Integer crntLevel = (device.currentValue("level") ?: 50).toInteger() 169 | Integer crntSwitch = (device.currentValue("switch") == "on") ? 1 : 0 170 | 171 | String switchText 172 | String levelText 173 | String switchValue 174 | 175 | switch(state.bin) { 176 | case -1: 177 | if (levelValue == 0){ 178 | switchValue = switchValues[0] 179 | switchText = "${switchVerbs[crntSwitch ^ 1]} ${switchValue}"// --c1" //xor 180 | } else { 181 | switchValue = switchValues[1] 182 | switchText = "${switchVerbs[crntSwitch & 1]} ${switchValue}"// --c3" 183 | if (levelValue == crntLevel) levelText = "${levelVerbs[1]} ${crntLevel}%"// --c3a" 184 | else levelText = "${levelVerbs[0]} ${levelValue}%"// --c3b" 185 | } 186 | break 187 | case 0..100: //digital set level -basic report 188 | switchValue = switchValues[levelValue ? 1 : 0] 189 | switchText = "${switchVerbs[crntSwitch & 1]} ${switchValue}"// --c4" 190 | if (levelValue == 0) levelValue = 1 191 | levelText = "${levelVerbs[levelValue == crntLevel ? 1 : 0]} ${levelValue}%"// --c4" 192 | break 193 | case -11: //digital on -basic report 194 | switchValue = switchValues[1] 195 | switchText = "${switchVerbs[crntSwitch & 1]} ${switchValue}"// --c5" 196 | break 197 | case -10: //digital off -basic report 198 | switchValue = switchValues[0] 199 | switchText = "${switchVerbs[crntSwitch ^ 1]} ${switchValue}"// --c6" 200 | break 201 | case -2: //refresh digital -basic report 202 | if (levelValue == 0){ 203 | switchValue = switchValues[0] 204 | switchText = "${switchVerbs[1]} ${switchValue}"// --c10" 205 | levelText = "${levelVerbs[1]} ${crntLevel}%"// --c10" 206 | levelValue = crntLevel 207 | } else { 208 | switchValue = switchValues[1] 209 | switchText = "${switchVerbs[1]} ${switchValue}"// --c11" 210 | levelText = "${levelVerbs[1]} ${levelValue}%"// --c11" 211 | } 212 | break 213 | default : 214 | log.debug "missing- bin: ${state.bin}, levelValue:${levelValue}, crntLevel: ${crntLevel}, crntSwitch: ${crntSwitch}, type: ${type}" 215 | break 216 | } 217 | 218 | if (switchText){ 219 | switchText = "${device.displayName} ${switchText} [${type}]" 220 | if (txtEnable) log.info "${switchText}" 221 | sendEvent(name: "switch", value: switchValue, descriptionText: switchText, type:type) 222 | } 223 | if (levelText){ 224 | levelText = "${device.displayName} ${levelText} [${type}]" 225 | if (txtEnable) log.info "${levelText}" 226 | sendEvent(name: "level", value: levelValue, descriptionText: levelText, type:type,unit:"%") 227 | } 228 | state.bin = -1 229 | } 230 | 231 | void delayHold(button){ 232 | sendButtonEvent("held", button, "physical") 233 | } 234 | 235 | void push(button){ 236 | sendButtonEvent("pushed", button, "digital") 237 | } 238 | 239 | void hold(button){ 240 | sendButtonEvent("held", button, "digital") 241 | } 242 | 243 | void release(button){ 244 | sendButtonEvent("released", button, "digital") 245 | } 246 | 247 | void doubleTap(button){ 248 | sendButtonEvent("doubleTapped", button, "digital") 249 | } 250 | 251 | void sendButtonEvent(action, button, type){ 252 | String descriptionText = "${device.displayName} button ${button} was ${action} [${type}]" 253 | if (txtEnable) log.info descriptionText 254 | sendEvent(name:action, value:button, descriptionText:descriptionText, isStateChange:true, type:type) 255 | } 256 | 257 | List setLevel(level){ 258 | return setLevel(level,1) 259 | } 260 | 261 | List setLevel(level,ramp){ 262 | state.flashing = false 263 | state.bin = level 264 | if (ramp > 255) ramp = 255 265 | else if (ramp == 0) ramp = 1 266 | Integer delay = (ramp * 1000) + 200 267 | if (level > 99) level = 99 268 | 269 | return [ 270 | secure(zwave.configurationV1.configurationSet(scaledConfigurationValue: ramp, parameterNumber: 8, size: 2)) 271 | ,"delay 200" 272 | ,secure(zwave.switchMultilevelV1.switchMultilevelSet(value: level)) 273 | ,"delay ${delay}" 274 | ,secure(zwave.basicV1.basicGet()) 275 | ] 276 | } 277 | 278 | 279 | List on(){ 280 | state.bin = -11 281 | state.flashing = false 282 | return delayBetween([ 283 | secure(zwave.switchMultilevelV1.switchMultilevelSet(value: 0xFF)) 284 | ,secure(zwave.basicV1.basicGet()) 285 | ] ,200) 286 | } 287 | 288 | List off(){ 289 | state.bin = -10 290 | state.flashing = false 291 | return delayBetween([ 292 | secure(zwave.switchMultilevelV1.switchMultilevelSet(value: 0x00)) 293 | ,secure(zwave.basicV1.basicGet()) 294 | ] ,200) 295 | } 296 | 297 | String flash(){ 298 | if (txtEnable) log.info "${device.displayName} was set to flash with a rate of ${flashRate ?: 750} milliseconds" 299 | state.flashing = true 300 | return flashOn() 301 | } 302 | 303 | String flashOn(){ 304 | if (!state.flashing) return 305 | runInMillis((flashRate ?: 750).toInteger(), flashOff) 306 | return secure(zwave.switchMultilevelV1.switchMultilevelSet(value: 0xFF)) 307 | } 308 | 309 | String flashOff(){ 310 | if (!state.flashing) return 311 | runInMillis((flashRate ?: 750).toInteger(), flashOn) 312 | return secure(zwave.switchMultilevelV1.switchMultilevelSet(value: 0x00)) 313 | } 314 | 315 | String refresh(){ 316 | if (logEnable) log.debug "refresh" 317 | state.bin = -2 318 | return secure(zwave.basicV1.basicGet()) 319 | } 320 | 321 | void installed(){ 322 | log.warn "installed..." 323 | sendEvent(name: "level", value: 20) 324 | } 325 | 326 | void configure(){ 327 | log.warn "configure..." 328 | runIn(1800,logsOff) 329 | sendEvent(name: "numberOfButtons", value: 2) 330 | state."${1}" = 0 331 | state."${2}" = 0 332 | runIn(5, "refresh") 333 | } 334 | 335 | //capture preference changes 336 | List updated(){ 337 | log.info "updated..." 338 | log.warn "debug logging is: ${logEnable == true}" 339 | log.warn "description logging is: ${txtEnable == true}" 340 | if (logEnable) runIn(1800,logsOff) 341 | 342 | List cmds = [] 343 | 344 | //paddle reverse function 345 | if (param4) { 346 | cmds.add(secure(zwave.configurationV1.configurationSet(scaledConfigurationValue: param4.toInteger(), parameterNumber: 4, size: 1))) 347 | } 348 | if (cmds) return cmds 349 | } 350 | -------------------------------------------------------------------------------- /examples/drivers/LifxColorBulbLegacy.groovy: -------------------------------------------------------------------------------- 1 | import hubitat.helper.ColorUtils 2 | import groovy.transform.Field 3 | 4 | metadata { 5 | definition (name: "LIFX Color Legacy", namespace: "hubitat", author: "Bryan Copeland") { 6 | capability "SwitchLevel" 7 | capability "ColorTemperature" 8 | capability "ColorControl" 9 | capability "Switch" 10 | capability "Refresh" 11 | capability "Actuator" 12 | capability "SignalStrength" 13 | capability "Configuration" 14 | capability "ColorMode" 15 | capability "Polling" 16 | 17 | command "flash" 18 | 19 | } 20 | preferences { 21 | input name: "pollInterval", type: "enum", title: "Poll Interval", defaultValue: 0, options: [0: "Disabled", 5: "5 Minutes", 10: "10 Minutes", 15: "15 Minutes", 30: "30 Minutes", 60: "1 Hour"], submitOnChange: true, width: 8 22 | input name: "colorStaging", type: "bool", description: "", title: "Enable color pre-staging", defaultValue: false 23 | input name: "colorTransition", type: "enum", description: "", title: "Transition Time", defaultValue: 0, options: [0: "ASAP", 1: "1 second", 2: "2 seconds", 3: "3 seconds", 4: "4 seconds", 5: "5 seconds", 6: "6 seconds", 7: "7 seconds", 8: "8 seconds", 9: "9 seconds", 10: "10 seconds"] 24 | input name: "flashRate", type: "enum", title: "Flash rate", options:[[750:"750ms"],[1000:"1s"],[2000:"2s"],[5000:"5s"]], defaultValue: 750 25 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true 26 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 27 | } 28 | } 29 | 30 | @Field static Map> targetColor = [:] 31 | 32 | Map getTargetColorVals() { 33 | if (!targetColor["${device.id}"]) { 34 | Map tempMap = [:] 35 | tempMap.hue = device.currentValue("hue").toInteger() 36 | tempMap.colorTemperature = device.currentValue("colorTemperature").toInteger() 37 | tempMap.level = device.currentValue("level").toInteger() 38 | tempMap.saturation = device.currentValue("saturation").toInteger() 39 | targetColor["${device.id}" as String] = tempMap 40 | } 41 | return targetColor["${device.id}"] 42 | } 43 | 44 | void setTargetColorVals(Map targetVals) { 45 | targetColor["${device.id}" as String] = targetVals 46 | } 47 | 48 | void logsOff(){ 49 | log.warn "debug logging disabled..." 50 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 51 | } 52 | 53 | void configure() { 54 | if (logEnable) log.debug "configure()" 55 | List cmds = [] 56 | cmds.add(new hubitat.lifx.commands.GetColor().format()) 57 | cmds.add(new hubitat.lifx.commands.SetLightPower(level: 65535, duration: tt).format()) 58 | cmds.add(new hubitat.lifx.commands.GetHostFirmware().format()) 59 | cmds.add(new hubitat.lifx.commands.GetWifiFirmware().format()) 60 | cmds.add(new hubitat.lifx.commands.GetVersion().format()) 61 | sendToDevice(cmds) 62 | refresh() 63 | if (logEnable) runIn(1800,logsOff) 64 | } 65 | 66 | void updated() { 67 | log.info "updated..." 68 | log.warn "debug logging is: ${logEnable == true}" 69 | log.warn "description logging is: ${txtEnable == true}" 70 | unschedule() 71 | if (pollInterval) { 72 | switch (pollInterval.toInteger()) { 73 | case 5: 74 | runEvery5Minutes("poll") 75 | break; 76 | case 10: 77 | runEvery10Minutes("poll") 78 | break; 79 | case 15: 80 | runEvery15Minutes("poll") 81 | break; 82 | case 30: 83 | runEvery30Minutes("poll") 84 | break; 85 | case 60: 86 | runEvery60Minutes("poll") 87 | break; 88 | } 89 | } 90 | if (logEnable) runIn(1800,logsOff) 91 | } 92 | 93 | void parse(String description) { 94 | if (logEnable) log.debug "parse:${description}" 95 | hubitat.lifx.Command cmd = hubitat.lifx.Lifx.parse(description) 96 | if (cmd) { 97 | lifxEvent(cmd) 98 | } 99 | } 100 | 101 | void eventProcess(Map evt) { 102 | if (device.currentValue(evt.name).toString() != evt.value.toString()) { 103 | if (!evt.descriptionText) { 104 | if (evt.unit != null) { 105 | evt.descriptionText = "${device.displayName} ${evt.name} is ${evt.value}${evt.unit}" 106 | } else { 107 | evt.descriptionText = "${device.displayName} ${evt.name} is ${evt.value}" 108 | } 109 | } 110 | sendEvent(evt) 111 | if (txtEnable) log.info evt.descriptionText 112 | } 113 | } 114 | 115 | void lifxEvent(hubitat.lifx.Command cmd) { 116 | if (logEnable) log.debug "Unhandled Command: ${cmd}" 117 | } 118 | 119 | void refresh() { 120 | if (logEnable) log.debug "refresh()" 121 | // manual refresh - clear target vals 122 | targetColor.remove("${device.id}") 123 | List cmds = [] 124 | cmds.add(new hubitat.lifx.commands.GetColor().format()) 125 | cmds.add(new hubitat.lifx.commands.GetWifiInfo().format()) 126 | cmds.add(new hubitat.lifx.commands.GetPower().format()) 127 | sendToDevice(cmds) 128 | } 129 | 130 | void privateRefresh() { 131 | // rapid commands have stopped - refresh target vals from new state 132 | targetColor.remove("${device.id}") 133 | List cmds = [] 134 | cmds.add(new hubitat.lifx.commands.GetColor().format()) 135 | cmds.add(new hubitat.lifx.commands.GetPower().format()) 136 | sendToDevice(cmds) 137 | } 138 | 139 | void poll() { 140 | privateRefresh() 141 | } 142 | 143 | void flash() { 144 | if (logEnable) log.debug "flash()" 145 | String descriptionText = "${device.getDisplayName()} was set to flash with a rate of ${flashRate ?: 750} milliseconds" 146 | if (txtEnable) log.info "${descriptionText}" 147 | state.flashing = true 148 | flashOn() 149 | } 150 | 151 | void flashOn() { 152 | if (!state.flashing) return 153 | runInMillis((flashRate ?: 750).toInteger(), flashOff) 154 | sendToDevice(new hubitat.lifx.commands.SetPower(level: 65535).format()) 155 | } 156 | 157 | void flashOff() { 158 | if (!state.flashing) return 159 | runInMillis((flashRate ?: 750).toInteger(), flashOn) 160 | sendToDevice(new hubitat.lifx.commands.SetPower(level: 0).format()) 161 | } 162 | 163 | void on() { 164 | state.flashing = false 165 | if (logEnable) log.debug "on()" 166 | Integer tt = 1000 167 | if (colorTransition) tt = colorTransition.toInteger() * 1000 168 | sendToDevice(new hubitat.lifx.commands.SetPower(level: 65535).format()) 169 | runIn((Math.round(tt/1000) + 1), privateRefresh) 170 | } 171 | 172 | void off() { 173 | state.flashing = false 174 | if (logEnable) log.debug "off()" 175 | Integer tt = 1000 176 | if (colorTransition) tt = colorTransition.toInteger() * 1000 177 | sendToDevice(new hubitat.lifx.commands.SetPower(level: 0).format()) 178 | runIn((Math.round(tt/1000) + 1), privateRefresh) 179 | } 180 | 181 | void setLevel(value, duration=null) { 182 | Map targetVals = getTargetColorVals() 183 | targetVals.level = value 184 | setTargetColorVals(targetVals) 185 | state.flashing = false 186 | List cmds = [] 187 | if (logEnable) log.debug "setLevel(${value})" 188 | Integer tt = 1000 189 | if (duration != null) { 190 | tt = duration * 1000 191 | } else { 192 | if (colorTransition) tt = colorTransition.toInteger() * 1000 193 | } 194 | cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(targetVals.hue * 655.35), saturation: Math.round(targetVals.saturation * 655.35), brightness: Math.round(targetVals.level * 655.35), kelvin: targetVals.colorTemperature, duration: tt).format()) 195 | if (device.currentValue("switch") != "on") { 196 | cmds.add(new hubitat.lifx.commands.SetPower(level: 65535).format()) 197 | } 198 | sendToDevice(cmds) 199 | runIn((Math.round(tt/1000) + 1), privateRefresh) 200 | } 201 | 202 | void setHue(value) { 203 | Map targetVals = getTargetColorVals() 204 | targetVals.hue = value 205 | targetVals.saturation = 100 206 | setTargetColorVals(targetVals) 207 | state.flashing = false 208 | List cmds = [] 209 | if (logEnable) log.debug "setHue(${value})" 210 | Integer tt = 1000 211 | if (colorTransition) tt = colorTransition.toInteger() * 1000 212 | cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(targetVals.hue * 655.35), saturation: Math.round(targetVals.saturation * 655.35), brightness: Math.round(targetVals.level * 655.35), kelvin: targetVals.colorTemperature, duration: tt).format()) 213 | if (!colorStaging && (device.currentValue("switch") != "on")) { 214 | cmds.add(new hubitat.lifx.commands.SetPower(level: 65535).format()) 215 | } 216 | sendToDevice(cmds) 217 | runIn((Math.round(tt/1000) + 1), privateRefresh) 218 | } 219 | 220 | void setSaturation(value) { 221 | Map targetVals = getTargetColorVals() 222 | targetVals.saturation = value 223 | setTargetColorVals(targetVals) 224 | state.flashing = false 225 | List cmds = [] 226 | if (logEnable) log.debug "setSaturation(${value})" 227 | Integer tt = 1000 228 | if (colorTransition) tt = colorTransition.toInteger() * 1000 229 | cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(targetVals.hue * 655.35), saturation: Math.round(targetVals.saturation * 655.35), brightness: Math.round(targetVals.level * 655.35), kelvin: targetVals.colorTemperature, duration: tt).format()) 230 | if (!colorStaging && (device.currentValue("switch") != "on")) { 231 | cmds.add(new hubitat.lifx.commands.SetPower(level: 65535).format()) 232 | } 233 | sendToDevice(cmds) 234 | runIn((Math.round(tt/1000) + 1), privateRefresh) 235 | } 236 | 237 | void setColor(value) { 238 | Map targetVals = getTargetColorVals() 239 | state.flashing = false 240 | List cmds = [] 241 | if (logEnable) log.debug "setColor(${value})" 242 | Integer tt = 1000 243 | if (colorTransition) tt = colorTransition.toInteger() * 1000 244 | if (value.hue == null || value.saturation == null) return 245 | if (value.level == null) value.level=100 246 | targetVals.saturation = value.saturation 247 | targetVals.hue = value.hue 248 | targetVals.level = value.level 249 | setTargetColorVals(targetVals) 250 | cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(targetVals.hue * 655.35), saturation: Math.round(targetVals.saturation * 655.35), brightness: Math.round(targetVals.level * 655.35), kelvin: targetVals.colorTemperature, duration: tt).format()) 251 | if (!colorStaging && (device.currentValue("switch") != "on")) { 252 | cmds.add(new hubitat.lifx.commands.SetPower(level: 65535).format()) 253 | } 254 | sendToDevice(cmds) 255 | runIn((Math.round(tt/1000) + 1), privateRefresh) 256 | } 257 | 258 | void setColorTemperature(Number temp, Number level=null, Number transitionTime=null) { 259 | Map targetVals = getTargetColorVals() 260 | state.flashing = false 261 | List cmds = [] 262 | Integer tt = 1000 263 | if (transitionTime == null) { 264 | if (colorTransition) tt = colorTransition.toInteger() * 1000 265 | } else { 266 | tt = transitionTime * 1000 267 | } 268 | if (logEnable) log.debug "setColorTemperature(${temp}, ${level}, ${transitionTime})" 269 | if (level == null) level = targetVals.level 270 | targetVals.colorTemperature = temp.toInteger() 271 | targetVals.saturation = 0 272 | targetVals.level = level.toInteger() 273 | setTargetColorVals(targetVals) 274 | cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(targetVals.hue * 655.35), saturation: Math.round(targetVals.saturation * 655.35), brightness: Math.round(targetVals.level * 655.35), kelvin: targetVals.colorTemperature, duration: tt).format()) 275 | if (!colorStaging && (device.currentValue("switch") != "on")) { 276 | cmds.add(new hubitat.lifx.commands.SetPower(level: 65535).format()) 277 | } 278 | sendToDevice(cmds) 279 | runIn((Math.round(tt/1000) + 1), privateRefresh) 280 | } 281 | 282 | void lifxEvent(hubitat.lifx.commands.StateWifiFirmware cmd) { 283 | if (logEnable) log.debug "${cmd}" 284 | Double fwVersion = cmd.versionMajor + (cmd.versionMinor / 100) 285 | device.updateDataValue("wifiFirmware", "${fwVersion}") 286 | } 287 | 288 | void lifxEvent(hubitat.lifx.commands.StateHostFirmware cmd) { 289 | if (logEnable) log.debug "${cmd}" 290 | Double fwVersion = cmd.versionMajor + (cmd.versionMinor / 100) 291 | device.updateDataValue("hostFirmware", "${fwVersion}") 292 | } 293 | 294 | void lifxEvent(hubitat.lifx.commands.StateVersion cmd) { 295 | if (logEnable) log.debug "${cmd}" 296 | device.updateDataValue("model: ", "${cmd.product}") 297 | Map lifxProduct = hubitat.helper.Lifx.lifxProducts[cmd.product] 298 | if (lifxProduct != null) { 299 | device.updateDataValue("modelName", lifxProduct.name) 300 | } 301 | } 302 | 303 | void lifxEvent(hubitat.lifx.commands.StateWifiInfo cmd) { 304 | Integer rssi = Math.floor(10 * Math.log10(cmd.signal.doubleValue()) + 0.5).toInteger() 305 | eventProcess(name: "rssi", value: rssi, unit: "dBm") 306 | } 307 | 308 | void lifxEvent(hubitat.lifx.commands.LightState cmd) { 309 | if (logEnable) log.debug cmd.toString() 310 | if (cmd.saturation > 0) { 311 | eventProcess(name: "colorMode", value: "RGB") 312 | setGenericName(Math.round(cmd.hue / 655.35)) 313 | } else { 314 | eventProcess(name: "colorMode", value: "CT") 315 | setGenericTempName(cmd.kelvin) 316 | } 317 | eventProcess(name: "hue", value: Math.round(cmd.hue / 655.35)) 318 | eventProcess(name: "saturation", value: Math.round(cmd.saturation / 655.35), unit: "%") 319 | eventProcess(name: "colorTemperature", value: cmd.kelvin, unit: "K") 320 | eventProcess(name: "level", value: Math.round(cmd.brightness / 655.35), unit: "%") 321 | eventProcess(name: "color", value: ColorUtils.rgbToHEX(ColorUtils.hsvToRGB([Math.round(cmd.hue / 655.35), Math.round(cmd.saturation / 655.35), Math.round(cmd.brightness / 655.35)]))) 322 | } 323 | 324 | void lifxEvent(hubitat.lifx.commands.StateLightPower cmd) { 325 | if (logEnable) log.debug cmd.toString() 326 | } 327 | 328 | void lifxEvent(hubitat.lifx.commands.StatePower cmd) { 329 | if (logEnable) log.debug cmd.toString() 330 | if (cmd.level > 0) { 331 | eventProcess(name: "switch", value: "on") 332 | } else { 333 | eventProcess(name: "switch", value: "off") 334 | } 335 | } 336 | 337 | private void setGenericTempName(temp){ 338 | if (!temp) return 339 | String genericName 340 | int value = temp.toInteger() 341 | if (value <= 2000) genericName = "Sodium" 342 | else if (value <= 2100) genericName = "Starlight" 343 | else if (value < 2400) genericName = "Sunrise" 344 | else if (value < 2800) genericName = "Incandescent" 345 | else if (value < 3300) genericName = "Soft White" 346 | else if (value < 3500) genericName = "Warm White" 347 | else if (value < 4150) genericName = "Moonlight" 348 | else if (value <= 5000) genericName = "Horizon" 349 | else if (value < 5500) genericName = "Daylight" 350 | else if (value < 6000) genericName = "Electronic" 351 | else if (value <= 6500) genericName = "Skylight" 352 | else if (value < 20000) genericName = "Polar" 353 | String descriptionText = "${device.getDisplayName()} color is ${genericName}" 354 | eventProcess(name: "colorName", value: genericName ,descriptionText: descriptionText) 355 | } 356 | 357 | private void setGenericName(hue){ 358 | String colorName 359 | hue = hue.toInteger() 360 | hue = (hue * 3.6) 361 | switch (hue.toInteger()){ 362 | case 0..15: colorName = "Red" 363 | break 364 | case 16..45: colorName = "Orange" 365 | break 366 | case 46..75: colorName = "Yellow" 367 | break 368 | case 76..105: colorName = "Chartreuse" 369 | break 370 | case 106..135: colorName = "Green" 371 | break 372 | case 136..165: colorName = "Spring" 373 | break 374 | case 166..195: colorName = "Cyan" 375 | break 376 | case 196..225: colorName = "Azure" 377 | break 378 | case 226..255: colorName = "Blue" 379 | break 380 | case 256..285: colorName = "Violet" 381 | break 382 | case 286..315: colorName = "Magenta" 383 | break 384 | case 316..345: colorName = "Rose" 385 | break 386 | case 346..360: colorName = "Red" 387 | break 388 | } 389 | String descriptionText = "${device.getDisplayName()} color is ${colorName}" 390 | eventProcess(name: "colorName", value: colorName ,descriptionText: descriptionText) 391 | } 392 | 393 | void sendToDevice(List cmds, Long delay = 300) { 394 | sendHubCommand(new hubitat.device.HubMultiAction(commands(cmds, delay), hubitat.device.Protocol.LIFX)) 395 | } 396 | 397 | void sendToDevice(String cmd, Long delay = 300) { 398 | sendHubCommand(new hubitat.device.HubAction(cmd, hubitat.device.Protocol.LIFX)) 399 | } 400 | 401 | List commands(List cmds, Long delay = 300) { 402 | return delayBetween(cmds.collect { it }, delay) 403 | } 404 | -------------------------------------------------------------------------------- /example-apps/autoDimmer.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Auto Dimmer V2.2 3 | * 4 | * Author: Mike Maxwell 5 | 6 | 2015-09-30 maxwell added dimmer specific level option (off) 7 | 2017-04-15 maxwell mods for Hubitat 8 | 2021-09-30 maxwell cleanup for publish 9 | 10 | This software if free for Private Use. You may use and modify the software without distributing it. 11 | 12 | This software and derivatives may not be used for commercial purposes. 13 | You may not distribute or sublicense this software. 14 | You may not grant a sublicense to modify and distribute this software to third parties not included in the license. 15 | 16 | Software is provided without warranty and the software author/license owner cannot be held liable for damages. 17 | 18 | */ 19 | import com.hubitat.app.DeviceWrapper 20 | import groovy.transform.Field 21 | 22 | @Field static Map luxDarkOpts = [ 23 | options:[10:"10 Lux",25:"25 Lux",50:"50 Lux",75:"75 Lux",100:"100 Lux"] 24 | ] 25 | @Field static Map luxDuskOpts = [ 26 | options:[100:"100 Lux",125:"125 Lux",150:"150 Lux",175:"175 Lux",200:"200 Lux",300:"300 Lux",400:"400 Lux",500:"500 Lux",600:"600 Lux"] 27 | ] 28 | @Field static Map luxBrightOpts = [ 29 | options:[500:"500 Lux",1000:"1000 Lux",2000:"2000 Lux",3000:"3000 Lux",4000:"4000 Lux"] 30 | ] 31 | @Field static Map dimDarkOpts = [ 32 | options:[10:"10%",20:"20%",30:"30%",40:"40%",50:"50%",60:"60%",70:"70%",80:"80%",90:"90%",100:"100%"] 33 | ] 34 | @Field static Map overrideDarkOpts = [ 35 | options:[0:"Off",5:"5%",10:"10%",20:"20%",25:"25%",30:"30%",35:"35%",40:"40%",45:"45%",50:"50%",60:"60%",70:"70%",80:"80%",90:"90%",100:"100%"] 36 | ] 37 | 38 | definition( 39 | name : "Auto Dimmer", 40 | namespace : "hubitat", 41 | author : "Mike Maxwell", 42 | description : "This add on smartApp automatically adjusts dimmer levels based on a lux sensor, fires from the switch on event.", 43 | category : "My Apps", 44 | iconUrl : "", 45 | iconX2Url : "", 46 | iconX3Url : "" 47 | ) 48 | 49 | preferences { 50 | page(name: "main") 51 | page(name: "aboutMe", nextPage : "main") 52 | page(name: "luxPage") 53 | page(name: "dimmersPage", nextPage : "main") 54 | page(name: "overridePage") 55 | page(name: "dimmerOptions") 56 | } 57 | 58 | void installed() { 59 | state.anyOptionsSet = false 60 | init() 61 | } 62 | 63 | void updated() { 64 | unsubscribe() 65 | init() 66 | } 67 | 68 | void init(){ 69 | subscribe(dimmers, "switch.on", dimHandler) 70 | subscribe(luxOmatic, "illuminance", luxHandler) 71 | } 72 | 73 | void dimHandler(evt) { 74 | setDimmer(evt.device,false) 75 | } 76 | 77 | void luxHandler(evt = "ramp"){ 78 | Boolean isAutoLux = false 79 | Boolean isPrestage = false 80 | dimmers.each { 81 | isAutoLux = settings."${it.deviceId}_autoLux" == true 82 | isPrestage = settings."${it.deviceId}_usePrestage" == true 83 | Boolean isOn = it.currentValue("switch") == "on" 84 | if (isAutoLux && isOn) { 85 | setDimmer(it,true) 86 | } else if (isPrestage && !isOn) { 87 | setDimmer(it,false) 88 | } 89 | } 90 | } 91 | 92 | String getCurrentLUXmode(){ 93 | Integer crntLux = luxOmatic?.currentValue("illuminance")?.toInteger() ?: 0 94 | state.luxValue = crntLux 95 | String lMode = "" 96 | if (crntLux == -1) lMode = "${crntLux}" 97 | else { 98 | if (crntLux < luxDark.toInteger()) { 99 | lMode = "Dark" 100 | } else if (crntLux < luxDusk.toInteger()) { 101 | lMode = "Dusk" 102 | } else if (crntLux < luxBright.toInteger()) { 103 | lMode = "Overcast" 104 | } else { 105 | lMode = "Bright" 106 | } 107 | } 108 | state.luxMode = lMode 109 | return lMode 110 | } 111 | 112 | void setDimmer(DeviceWrapper dimmer,Boolean isRamp){ 113 | Integer newLevel = 0 114 | 115 | //get its current dim level 116 | Integer crntDimmerLevel = dimmer.currentValue("level").toInteger() 117 | if (crntDimmerLevel >= 99) crntDimmerLevel = 100 118 | 119 | //get currentLux reading 120 | Integer crntLux = luxOmatic.currentValue("illuminance").toInteger() 121 | 122 | String prefVar = dimmer.deviceId.toString() 123 | String dimVar 124 | if (crntLux < luxDark.toInteger()) { 125 | prefVar = prefVar + "_dark" 126 | dimVar = dimDark 127 | } else if (crntLux < luxDusk.toInteger()) { 128 | prefVar = prefVar + "_dusk" 129 | dimVar = dimDusk 130 | } else if (crntLux < luxBright.toInteger()) { 131 | prefVar = prefVar + "_day" 132 | dimVar = dimDay 133 | } else { 134 | prefVar = prefVar + "_bright" 135 | dimVar = dimBright 136 | } 137 | 138 | Integer newDimmerLevel = (this."${prefVar}" ?: dimVar).toInteger() 139 | if (newDimmerLevel >= 99) newDimmerLevel = 100 140 | 141 | if ( newDimmerLevel == crntDimmerLevel ){ 142 | //log.info "${dimmer.displayName}, changeRequired: False" 143 | } else { 144 | if (isRamp) { 145 | if (newDimmerLevel == 0){ 146 | log.info "${dimmer.displayName}, currentLevel:${crntDimmerLevel}%, requestedLevel:${newDimmerLevel}%, currentLux:${crntLux}" 147 | dimmer.off() 148 | } else { 149 | String rampRate = dimmer.deviceId.toString() 150 | rampRate = rampRate + "_ramp" 151 | Integer rampInt = (this."${rampRate}" ?: 2).toInteger() 152 | Integer rampLevel 153 | if (crntDimmerLevel < newDimmerLevel){ 154 | rampLevel = Math.min(crntDimmerLevel + rampInt, newDimmerLevel) 155 | //on the way up if ramp level >= 99 push it to 100 156 | if (rampLevel >= 99) rampLevel = 100 157 | } else { 158 | //we could be at 100, with a request to go down, so we need a minimum ramp of 2 to get around the 99 issue 159 | if (crntDimmerLevel == 100) rampInt = Math.max(rampInt,2) 160 | rampLevel = Math.max(crntDimmerLevel - rampInt,newDimmerLevel) 161 | } 162 | dimmer.setLevel(rampLevel) 163 | if (rampLevel != newDimmerLevel){ 164 | runIn(60,luxHandler) 165 | } //else { log.debug "bailed on run in" } 166 | } 167 | } else { 168 | if (newDimmerLevel == 0){ 169 | dimmer.off() 170 | } else { 171 | dimmer.setLevel(newDimmerLevel) 172 | } 173 | 174 | } 175 | } 176 | } 177 | 178 | void setCurrentOverride(Integer did, String darkValue, String duskValue, String dayValue, String brightValue){ 179 | //get current mode, and the input value from above 180 | String crntModeValue 181 | switch (state.luxMode){ 182 | case "Dark": 183 | if (darkValue != null) crntModeValue = darkValue 184 | break 185 | case "Dusk": 186 | if (duskValue != null) crntModeValue = duskValue 187 | break 188 | case "Overcast": 189 | if (dayValue != null) crntModeValue = dayValue 190 | break 191 | case "Bright": 192 | if (brightValue != null) crntModeValue = brightValue 193 | break 194 | } 195 | if (crntModeValue == null) return 196 | 197 | DeviceWrapper dimmer = dimmers.find{ it.deviceId == did.toInteger() } 198 | //see if we need to do anything 199 | String dState = dimmer.currentValue("switch") 200 | Integer dValue = dimmer.currentValue("level") 201 | if (dState == "on"){ 202 | if (dValue != crntModeValue.toInteger()) { 203 | dimmer.setLevel(crntModeValue.toInteger()) 204 | } 205 | } 206 | } 207 | 208 | /* page methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 209 | Map main(){ 210 | Boolean isLuxComplete = luxPageComplete() == "complete" 211 | Boolean isDimComplete = dimmersPageComplete() == "complete" 212 | Boolean allComplete = isLuxComplete && isDimComplete 213 | String toDo 214 | String pageTitle = "Setup..." 215 | if (!isLuxComplete){ 216 | toDo = "luxPage" 217 | } else if (!isDimComplete){ 218 | toDo = "dimmersPage" 219 | } else { 220 | pageTitle = "Current LUX Mode: ${getCurrentLUXmode()}" 221 | } 222 | 223 | return dynamicPage( 224 | name : "main" 225 | ,title : pageTitle 226 | ,nextPage : toDo 227 | ,install : allComplete 228 | ,uninstall : true 229 | ){ 230 | section(){ 231 | href( 232 | name : "ab" 233 | ,title : "About Me..." 234 | ,required : false 235 | ,page : "aboutMe" 236 | ,description: "" 237 | ) 238 | if (isLuxComplete){ 239 | href( 240 | name : "lp" 241 | ,title : "Illuminance settings..." 242 | ,required : false 243 | ,page : "luxPage" 244 | ,description: "" 245 | ,state : luxPageComplete() 246 | ) 247 | } 248 | if (isDimComplete){ 249 | href( 250 | name : "dp" 251 | ,title : "Dimmers and defaults..." 252 | ,required : false 253 | ,page : "dimmersPage" 254 | ,description: "" 255 | ,state : dimmersPageComplete() 256 | ) 257 | } 258 | if (allComplete){ 259 | href( 260 | name : "op" 261 | ,title : "Specific dimmer settings..." 262 | ,required : false 263 | ,page : "overridePage" 264 | ,description: "" 265 | ,state : anyOptionsSet() 266 | ) 267 | } 268 | } 269 | } 270 | } 271 | 272 | Map aboutMe(){ 273 | return dynamicPage(name: "aboutMe"){ 274 | section ("About Me"){ 275 | paragraph "This add on smartApp automatically adjusts dimmer levels when dimmer(s) are turned on from physical switches or other smartApps.\n" + 276 | "Levels are set based on lux (illuminance) sensor readings and the dimmer levels that you specify." + 277 | "This smartApp does not turn on dimmers directly, this allows you to retain all your existing on/off smartApps.\n"+ 278 | "autoDimmer provides intelligent level management to your existing automations." 279 | } 280 | } 281 | } 282 | 283 | Map luxPage(){ 284 | Boolean isDimComplete = dimmersPageComplete() == "complete" 285 | String toDo 286 | if (!isDimComplete){ 287 | toDo = "dimmersPage" 288 | } else if (!isDimComplete){ 289 | toDo = "main" 290 | } 291 | String info = "" 292 | if (state.luxMode && state.luxValue) info = "Current Mode: ${state.luxMode}, LUX: ${state.luxValue}" 293 | return dynamicPage(name: "luxPage",nextPage: toDo){ 294 | section ("Illuminance settings"){ 295 | input( 296 | name : "luxOmatic" 297 | ,title : "Use this illuminance Sensor..." 298 | ,multiple : false 299 | ,required : true 300 | ,type : "capability.illuminanceMeasurement" 301 | ,submitOnChange : true 302 | ) 303 | input(name: "test",title: "test",type: "button") 304 | } 305 | section("Select Lux levels"){ 306 | paragraph "${info}" 307 | input( 308 | name : "luxDark" 309 | ,title : "It's Dark below this level..." 310 | ,multiple : false 311 | ,required : true 312 | ,type : "enum" 313 | ,options : luxDarkOpts.options 314 | ) 315 | input( 316 | name : "luxDusk" 317 | ,title : "Dusk/Dawn is between Dark and here..." 318 | ,multiple : false 319 | ,required : true 320 | ,type : "enum" 321 | ,options : luxDuskOpts.options 322 | ) 323 | input( 324 | name : "luxBright" 325 | ,title : "Overcast is between Dusk/Dawn and here, above this level it's considered Sunny." 326 | ,multiple : false 327 | ,required : true 328 | ,type : "enum" 329 | ,options : luxBrightOpts.options 330 | ) 331 | } 332 | } 333 | } 334 | 335 | Map dimmersPage(){ 336 | String info = "" 337 | if (state.luxMode && state.luxValue) info = "Current Mode: ${state.luxMode}, LUX: ${state.luxValue}" 338 | return dynamicPage(name: "dimmersPage",title: "Dimmers and defaults"){ 339 | section ("Default dim levels for each brigtness range"){ 340 | paragraph "${info}" 341 | input( 342 | name : "dimDark" 343 | ,title : "When it's Dark out..." 344 | ,multiple : false 345 | ,required : true 346 | ,type : "enum" 347 | ,options : dimDarkOpts.options 348 | ) 349 | input( 350 | name : "dimDusk" 351 | ,title : "For Dusk/Dawn use this..." 352 | ,multiple : false 353 | ,required : true 354 | ,type : "enum", 355 | ,options : dimDarkOpts.options 356 | ) 357 | input( 358 | name : "dimDay" 359 | ,title : "When it's Overcast..." 360 | ,multiple : false 361 | ,required : true 362 | ,type : "enum" 363 | ,options : dimDarkOpts.options 364 | ) 365 | input( 366 | name : "dimBright" 367 | ,title : "When it's Bright..." 368 | ,multiple : false 369 | ,required : true 370 | ,type : "enum" 371 | ,options : dimDarkOpts.options 372 | ) 373 | } 374 | section (){ 375 | input( 376 | name : "dimmers" 377 | ,multiple : true 378 | ,required : true 379 | ,submitOnChange : true 380 | ,type : "capability.switchLevel" 381 | ,title : "Dimmers to manage" 382 | ) 383 | } 384 | } 385 | } 386 | 387 | Map overridePage(){ 388 | state.anyOptionsSet = false 389 | return dynamicPage(name: "overridePage"){ 390 | section("Specific dimmer settings"){ 391 | List sortedDimmers = dimmers.sort{it.displayName} 392 | sortedDimmers.each() { dimmer -> 393 | def safeName = dimmer.id 394 | def name = dimmer.displayName 395 | 396 | href( 397 | name : safeName + "_pg" 398 | ,title : name 399 | ,required : false 400 | ,page : "dimmerOptions" 401 | ,params : [id:safeName,displayName:name] 402 | ,description: "" 403 | ,state : dimmerOptionsSelected(safeName) 404 | ) 405 | } 406 | } 407 | } 408 | } 409 | 410 | Map dimmerOptions(params){ 411 | Integer safeName 412 | String displayName 413 | if (params.id) { 414 | safeName = params.id.toInteger() 415 | displayName = params.displayName 416 | } else if (params.params) { 417 | safeName = params.params.id.toInteger() 418 | displayName = params.params.displayName 419 | } 420 | String pageTitle = "${displayName} Options, Current LUX Mode: ${state.luxMode}" 421 | 422 | setCurrentOverride(safeName,this."${safeName}_dark",this."${safeName}_dusk",this."${safeName}_day",this."${safeName}_bright") //132,30,null,null,null 423 | 424 | String titleDark = "Dark level [default: ${dimDark}]" 425 | String titleDusk = "Dusk/Dawn level [default: ${dimDusk}]" 426 | String titleDay = "Overcast level [default: ${dimDay}]" 427 | String titleBright = "Bright level [default: ${dimBright}]" 428 | return dynamicPage(name: "dimmerOptions") { 429 | section("${pageTitle}") { 430 | input( 431 | name : safeName + "_autoLux" 432 | ,title : "Auto adjust levels during LUX changes when dimmer is on" 433 | ,required : false 434 | ,type : "bool" 435 | ) 436 | input( 437 | name : safeName + "_usePrestage" 438 | ,title : "Use level prestage when dimmer is off (must be enabled on device)" 439 | ,required : false 440 | ,type : "bool" 441 | ) 442 | input( 443 | name : safeName + "_ramp" 444 | ,title : "Percent rate of change for Auto adjust" 445 | ,multiple : false 446 | ,required : false 447 | ,type : "enum" 448 | ,options : [["1":"1%"],["2":"2%"],["5":"5%"]] 449 | ,defaultValue : "2" 450 | ) 451 | } 452 | section("Set these to override the default settings."){ 453 | input( 454 | name : safeName + "_dark" 455 | ,title : titleDark //"Dark level" 456 | ,multiple : false 457 | ,required : false 458 | ,type : "enum" 459 | ,options : overrideDarkOpts.options 460 | ,submitOnChange : true 461 | ) 462 | input( 463 | name : safeName + "_dusk" 464 | ,title : titleDusk //"Dusk/Dawn level" 465 | ,multiple : false 466 | ,required : false 467 | ,type : "enum" 468 | ,options : overrideDarkOpts.options 469 | ,submitOnChange : true 470 | ) 471 | input( 472 | name : safeName + "_day" 473 | ,title : titleDay //"Overcast level" 474 | ,multiple : false 475 | ,required : false 476 | ,type : "enum" 477 | ,options : overrideDarkOpts.options 478 | ,submitOnChange : true 479 | ) 480 | input( 481 | name : safeName + "_bright" 482 | ,title : titleBright //"Bright level" 483 | ,multiple : false 484 | ,required : false 485 | ,type : "enum" 486 | ,options : overrideDarkOpts.options 487 | ,submitOnChange : true 488 | ) 489 | } 490 | } 491 | } 492 | 493 | /* href methods * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 494 | String dimmersPageComplete(){ 495 | if (dimmers && dimDark && dimDusk && dimDay && dimBright){ 496 | return "complete" 497 | } else { 498 | return "" 499 | } 500 | } 501 | 502 | String dimmerOptionsSelected(safeName){ 503 | def optionsList = ["${safeName}_autoLux","${safeName}_dark","${safeName}_dusk","${safeName}_day","${safeName}_bright"] 504 | if (optionsList.find{this."${it}"}){ 505 | state.anyOptionsSet = true 506 | return "complete" 507 | } else { 508 | return "" 509 | } 510 | } 511 | 512 | String anyOptionsSet(){ 513 | if (state.anyOptionsSet) { 514 | return "complete" 515 | } else { 516 | return "" 517 | } 518 | } 519 | 520 | String luxPageComplete(){ 521 | if (luxOmatic && luxDark && luxDusk && luxBright){ 522 | return "complete" 523 | } else { 524 | return "" 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /examples/drivers/haloSmokeCoDetector.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | Halo Smoke Alarm 3 | 4 | Copyright 2016, 2017, 2018, 2019 Hubitat Inc. All Rights Reserved 5 | 2019-02-12 2.0.6 maxwell 6 | -update enrollResponse with delay 7 | 2018-09-28 maxwell 8 | -initial pub 9 | 10 | */ 11 | 12 | import hubitat.zigbee.clusters.iaszone.ZoneStatus 13 | 14 | metadata { 15 | definition (name: "Halo Smoke Alarm", namespace: "hubitat", author: "Mike Maxwell") { 16 | capability "Configuration" 17 | capability "Switch" 18 | capability "Switch Level" 19 | capability "Color Control" 20 | capability "Relative Humidity Measurement" 21 | capability "Temperature Measurement" 22 | capability "Pressure Measurement" 23 | capability "Smoke Detector" 24 | capability "Carbon Monoxide Detector" 25 | capability "Refresh" 26 | capability "Sensor" 27 | capability "Actuator" 28 | 29 | fingerprint inClusters: "0000,0001,0003,0402,0403,0405,0500,0502", manufacturer: "HaloSmartLabs", model: "haloWX", deviceJoinName: "Halo Smoke Alarm" 30 | 31 | } 32 | preferences { 33 | input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true 34 | input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true 35 | } 36 | } 37 | 38 | def logsOff(){ 39 | log.warn "debug logging disabled..." 40 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 41 | } 42 | 43 | def updated(){ 44 | log.info "updated..." 45 | log.warn "debug logging is: ${logEnable == true}" 46 | log.warn "description logging is: ${txtEnable == true}" 47 | if (logEnable) runIn(1800,logsOff) 48 | } 49 | 50 | def installed(){ 51 | sendEvent(name:"smoke", value:"clear") 52 | sendEvent(name:"carbonMonoxide", value:"clear") 53 | } 54 | 55 | def parse(String description) { 56 | if (logEnable) log.debug "parse: ${description}" 57 | 58 | def result = [] 59 | 60 | if (description?.startsWith("enroll request")) { 61 | List cmds = zigbee.enrollResponse(1200) 62 | result = cmds?.collect { new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE) } 63 | } else { 64 | if (description?.startsWith("zone status")) { 65 | result = parseIasMessage(description) 66 | } else { 67 | result = parseReportAttributeMessage(description) 68 | } 69 | } 70 | return result 71 | } 72 | 73 | private parseReportAttributeMessage(String description) { 74 | def descMap = zigbee.parseDescriptionAsMap(description) 75 | if (logEnable) log.debug "zigbee.parseDescriptionAsMap-read attr: ${descMap}" 76 | def result = [] 77 | def cluster = descMap.cluster ?: descMap.clusterId 78 | switch(cluster) { 79 | case "0405": 80 | getHumidityResult(descMap.value) 81 | break 82 | case "0402": 83 | getTemperatureResult(descMap.value) 84 | break 85 | case "0403": 86 | getPressureResult(descMap.value) 87 | break 88 | case "FD00": 89 | getAlarmResult(descMap.data[0]) 90 | break 91 | /* 92 | case "FD01": // 93 | log.debug "FD01- cmd:${descMap.command}, data:${descMap.data}" 94 | break 95 | case "FD02": // 96 | //data:[03] ?? 97 | log.debug "FD02- cmd:${descMap.command}, data:${descMap.data}" 98 | break 99 | case "0500": // 100 | // cmd:04, data:[00] 101 | log.debug "0500- cmd:${descMap.command}, data:${descMap.data}" 102 | break 103 | */ 104 | case "0006": //switch events 105 | getSwitchResult(descMap.value) 106 | break 107 | case "0008": //level Events 108 | getLevelResult(descMap.value) 109 | break 110 | case "0300": //color events 111 | getColorResult(descMap.value,descMap.attrInt) 112 | break 113 | default : 114 | if (logEnable) log.warn "parseReportAttributeMessage- skip descMap:${descMap}, description:${description}" 115 | break 116 | } 117 | 118 | return result 119 | } 120 | 121 | private parseIasMessage(String description) { 122 | ZoneStatus zs = zigbee.parseZoneStatus(description) 123 | 124 | if (zs.alarm1) getAlarmResult("AL1") 125 | if (zs.alarm2) getAlarmResult("AL2") 126 | if (zs.alarm1 == 0 && zs.alarm2 == 0) getAlarmResult("00") 127 | 128 | } 129 | 130 | private getAlarmResult(rawValue){ 131 | if (rawValue == null) return 132 | def value 133 | def name 134 | def descriptionText = "${device.displayName} " 135 | switch (rawValue) { 136 | case "00": //cleared 137 | if (device.currentValue("smoke") == "detected") { 138 | descriptionText = "${descriptionText} smoke is clear" 139 | sendEvent(name:"smoke", value:"clear",descriptionText:descriptionText) 140 | } else if (device.currentValue("carbonMonoxide") == "detected") { 141 | descriptionText = "${descriptionText} carbon monoxide is clear" 142 | sendEvent(name:"carbonMonoxide", value:"clear",descriptionText:descriptionText) 143 | } else { 144 | descriptionText = "${descriptionText} smoke and carbon monoxide are clear" 145 | sendEvent(name:"smoke", value:"clear") 146 | sendEvent(name:"carbonMonoxide", value:"clear") 147 | } 148 | break 149 | /* 150 | case "04": //Elevated Smoke Detected (pre) 151 | descriptionText = "${descriptionText} smoke was detected (pre alert)" 152 | sendEvent(name:"smoke", value:"detected",descriptionText:"${descriptionText} smoke was detected") 153 | descriptionText = "${descriptionText} smoke was detected (pre alert)" 154 | break 155 | case "07": //Smoke Detected, send again, force state change 156 | sendEvent(name:"smoke", value:"detected",descriptionText:"${descriptionText} smoke was detected", isStateChange: true) 157 | descriptionText = "${descriptionText} smoke was detected" 158 | break 159 | */ 160 | case "09": //Silenced 161 | log.debug "getAlarmResult- Silenced" 162 | break 163 | case "AL1": 164 | descriptionText = "${descriptionText} smoke was detected" 165 | sendEvent(name:"smoke", value:"detected",descriptionText:descriptionText, isStateChange: true) 166 | break 167 | case "AL2": 168 | descriptionText = "${descriptionText} carbon monoxide was detected" 169 | sendEvent(name:"carbonMonoxide", value:"detected",descriptionText:descriptionText, isStateChange: true) 170 | break 171 | default : 172 | descriptionText = "${descriptionText} getAlarmResult- skipped:${value}" 173 | return 174 | } 175 | if (txtEnable) log.info "${descriptionText}" 176 | } 177 | 178 | private getHumidityResult(valueRaw){ 179 | if (valueRaw == null) return 180 | def value = Integer.parseInt(valueRaw, 16) / 100 181 | def descriptionText = "${device.displayName} RH is ${value}%" 182 | if (txtEnable) log.info "${descriptionText}" 183 | sendEvent(name:"humidity", value:value, descriptionText:descriptionText, unit:"%") 184 | } 185 | 186 | private getPressureResult(hex){ 187 | if (hex == null) return 188 | def valueRaw = hexStrToUnsignedInt(hex) 189 | def value = valueRaw / 10 190 | def descriptionText = "${device.displayName} pressure is ${value}kPa" 191 | if (txtEnable) log.info "${descriptionText}" 192 | sendEvent(name:"pressure", value:value, descriptionText:descriptionText, unit:"kPa") 193 | } 194 | 195 | private getTemperatureResult(valueRaw){ 196 | if (valueRaw == null) return 197 | def tempC = hexStrToSignedInt(valueRaw) / 100 198 | def value = convertTemperatureIfNeeded(tempC.toFloat(),"c",2) 199 | def unit = "°${location.temperatureScale}" 200 | def descriptionText = "${device.displayName} temperature is ${value}${unit}" 201 | if (txtEnable) log.info "${descriptionText}" 202 | sendEvent(name:"temperature",value:value, descriptionText:descriptionText, unit:unit) 203 | } 204 | 205 | private getSwitchResult(rawValue){ 206 | def value = rawValue == "01" ? "on" : "off" 207 | def name = "switch" 208 | if (device.currentValue("${name}") == value){ 209 | descriptionText = "${device.displayName} is ${value}" 210 | } else { 211 | descriptionText = "${device.displayName} was turned ${value}" 212 | } 213 | if (txtEnable) log.info "${descriptionText}" 214 | sendEvent(name:name,value:value,descriptionText:descriptionText) 215 | } 216 | 217 | private getLevelResult(rawValue){ 218 | def unit = "%" 219 | def value = Math.round(Integer.parseInt(rawValue,16) / 2.55) 220 | def name = "level" 221 | if (device.currentValue("${name}") == value){ 222 | descriptionText = "${device.displayName} is ${value}${unit}" 223 | } else { 224 | descriptionText = "${device.displayName} was set to ${value}${unit}" 225 | } 226 | if (txtEnable) log.info "${descriptionText}" 227 | sendEvent(name:name,value:value,descriptionText:descriptionText,unit:unit) 228 | } 229 | 230 | private getColorResult(rawValue,attrInt){ 231 | def unit = "%" 232 | def value = Math.round(Integer.parseInt(rawValue,16) / 2.55) 233 | def name 234 | switch (attrInt){ 235 | case 0: //hue 236 | name = "hue" 237 | if (device.currentValue("${name}")?.toInteger() == value){ 238 | descriptionText = "${device.displayName} ${name} is ${value}${unit}" 239 | } else { 240 | descriptionText = "${device.displayName} ${name} was set to ${value}${unit}" 241 | } 242 | state.lastHue = rawValue 243 | break 244 | case 1: //sat 245 | name = "saturation" 246 | if (device.currentValue("${name}")?.toInteger() == value){ 247 | descriptionText = "${device.displayName} ${name} is ${value}${unit}" 248 | } else { 249 | descriptionText = "${device.displayName} ${name} was set to ${value}${unit}" 250 | } 251 | state.lastSaturation = rawValue 252 | break 253 | default : 254 | return 255 | } 256 | if (txtEnable) log.info "${descriptionText}" 257 | sendEvent(name:name,value:value,descriptionText:descriptionText,unit:unit) 258 | } 259 | 260 | def configure() { 261 | log.warn "configure..." 262 | runIn(1800,logsOff) 263 | 264 | def cmds = [ 265 | //bindings 266 | "zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 200", //temp 267 | "zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 200", //pressure 268 | "zdo bind 0x${device.deviceNetworkId} 1 1 0x0405 {${device.zigbeeId}} {}", "delay 200", //hum 269 | "zdo bind 0x${device.deviceNetworkId} 2 1 0x0006 {${device.zigbeeId}} {}", "delay 200", 270 | "zdo bind 0x${device.deviceNetworkId} 2 1 0x0008 {${device.zigbeeId}} {}", "delay 200", 271 | "zdo bind 0x${device.deviceNetworkId} 2 1 0x0300 {${device.zigbeeId}} {}", "delay 200", 272 | //mfr specific 273 | "zdo bind 0x${device.deviceNetworkId} 4 1 0xFD00 {${device.zigbeeId}} {1201}", "delay 200", //appears to be custom alarm 274 | "zdo bind 0x${device.deviceNetworkId} 4 1 0xFD01 {${device.zigbeeId}} {1201}", "delay 200", //hush??? 275 | "zdo bind 0x${device.deviceNetworkId} 4 1 0xFD02 {${device.zigbeeId}} {1201}", "delay 200", 276 | //reporting 277 | "he cr 0x${device.deviceNetworkId} 1 0x0402 0x0000 0x29 1 43200 {50}","delay 200", //temp 278 | "he cr 0x${device.deviceNetworkId} 1 0x0405 0x0000 0x21 1 43200 {50}","delay 200", //hum 279 | "he cr 0x${device.deviceNetworkId} 1 0x0403 0x0000 0x29 1 43200 {1}","delay 200", //pressure 280 | //mfr specific reporting 281 | "he cr 0x${device.deviceNetworkId} 0x04 0xFD00 0x0000 0x30 5 120 {} {1201}","delay 200", //alarm 282 | "he cr 0x${device.deviceNetworkId} 0x04 0xFD00 0x0001 0x30 5 120 {} {1201}","delay 200", //alarm 283 | //need to verify ep, st attrib id's 1, and 0 284 | "he cr 0x${device.deviceNetworkId} 0x01 0xFD01 0x0000 0x30 5 120 {} {1201}","delay 200", //hush??? 285 | "he cr 0x${device.deviceNetworkId} 0x01 0xFD01 0x0001 0x0A 5 120 {} {1201}","delay 200", //hush??? 286 | "he cr 0x${device.deviceNetworkId} 0x01 0xFD02 0x0000 0x29 5 120 {} {1201}","delay 200", //no idea... 287 | 288 | ] + zigbee.enrollResponse(1200) + refresh() 289 | return cmds 290 | } 291 | 292 | def on() { 293 | def cmd = [ 294 | "he cmd 0x${device.deviceNetworkId} 0x02 0x0006 1 {}","delay 200", 295 | "he rattr 0x${device.deviceNetworkId} 2 0x0006 0 {}" 296 | ] 297 | return cmd 298 | } 299 | 300 | def off() { 301 | def cmd = [ 302 | "he cmd 0x${device.deviceNetworkId} 0x02 0x0006 0 {}","delay 200", 303 | "he rattr 0x${device.deviceNetworkId} 2 0x0006 0 {}" 304 | ] 305 | return cmd 306 | } 307 | 308 | def setLevel(value) { 309 | setLevel(value,(transitionTime?.toBigDecimal() ?: 1000) / 1000) 310 | } 311 | 312 | def setLevel(value,rate) { 313 | rate = rate.toBigDecimal() 314 | def scaledRate = (rate * 10).toInteger() 315 | def cmd = [] 316 | def isOn = device.currentValue("switch") == "on" 317 | value = (value.toInteger() * 2.55).toInteger() 318 | if (isOn){ 319 | cmd = [ 320 | "he cmd 0x${device.deviceNetworkId} 2 0x0008 4 {0x${intTo8bitUnsignedHex(value)} 0x${intTo16bitUnsignedHex(scaledRate)}}", 321 | "delay ${(rate * 1000) + 400}", 322 | "he rattr 0x${device.deviceNetworkId} 2 0x0008 0 {}" 323 | ] 324 | } else { 325 | cmd = [ 326 | "he cmd 0x${device.deviceNetworkId} 2 0x0008 4 {0x${intTo8bitUnsignedHex(value)} 0x0100}", "delay 200", 327 | "he rattr 0x${device.deviceNetworkId} 2 0x0006 0 {}", "delay 200", 328 | "he rattr 0x${device.deviceNetworkId} 2 0x0008 0 {}" 329 | ] 330 | } 331 | return cmd 332 | } 333 | 334 | 335 | def setColor(value){ 336 | if (value.hue == null || value.saturation == null) return 337 | 338 | def rate = transitionTime?.toInteger() ?: 1000 339 | def isOn = device.currentValue("switch") == "on" 340 | 341 | def hexSat = zigbee.convertToHexString(Math.round(value.saturation.toInteger() * 254 / 100).toInteger(),2) 342 | def level 343 | if (value.level) level = (value.level.toInteger() * 2.55).toInteger() 344 | def cmd = [] 345 | def hexHue 346 | 347 | hexHue = zigbee.convertToHexString(Math.round(value.hue * 254 / 100).toInteger(),2) 348 | 349 | if (isOn){ 350 | if (value.level){ 351 | cmd = [ 352 | "he cmd 0x${device.deviceNetworkId} 2 0x0300 0x06 {${hexHue} ${hexSat} ${intTo16bitUnsignedHex(rate / 100)}}","delay 200", 353 | "he cmd 0x${device.deviceNetworkId} 2 0x0008 4 {0x${intTo8bitUnsignedHex(level)} 0x${intTo16bitUnsignedHex(rate / 100)}}", 354 | "delay ${rate + 400}", 355 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0000 {}", "delay 200", 356 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0001 {}", "delay 200", 357 | "he rattr 0x${device.deviceNetworkId} 2 0x0008 0 {}" 358 | ] 359 | } else { 360 | cmd = [ 361 | "he cmd 0x${device.deviceNetworkId} 2 0x0300 0x06 {${hexHue} ${hexSat} ${intTo16bitUnsignedHex(rate / 100)}}", 362 | "delay ${rate + 400}", 363 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0000 {}", "delay 200", 364 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0001 {}" 365 | ] 366 | } 367 | } else if (level){ 368 | cmd = [ 369 | "he cmd 0x${device.deviceNetworkId} 2 0x0300 0x06 {${hexHue} ${hexSat} 0x0100}", "delay 200", 370 | "he cmd 0x${device.deviceNetworkId} 2 0x0008 4 {0x${intTo8bitUnsignedHex(level)} 0x0100}", "delay 200", 371 | "he rattr 0x${device.deviceNetworkId} 2 0x0006 0 {}", "delay 200", 372 | "he rattr 0x${device.deviceNetworkId} 2 0x0008 0 {}", "delay 200", 373 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0000 {}", "delay 200", 374 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0001 {}" 375 | ] 376 | } else { 377 | cmd = [ 378 | "he cmd 0x${device.deviceNetworkId} 2 0x0300 0x06 {${hexHue} ${hexSat} 0x0100}", "delay 200", 379 | "he cmd 0x${device.deviceNetworkId} 2 0x0006 1 {}","delay 200", 380 | "he rattr 0x${device.deviceNetworkId} 2 0x0006 0 {}","delay 200", 381 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0000 {}", "delay 200", 382 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0001 {}" 383 | ] 384 | } 385 | state.lastSaturation = hexSat 386 | state.lastHue = hexHue 387 | return cmd 388 | } 389 | 390 | def setHue(value) { 391 | def hexHue 392 | def rate = 1000 393 | def isOn = device.currentValue("switch") == "on" 394 | hexHue = zigbee.convertToHexString(Math.round(value * 254 / 100).toInteger(),2) 395 | def hexSat = state.lastSaturation 396 | def cmd = [] 397 | if (isOn){ 398 | cmd = [ 399 | "he cmd 0x${device.deviceNetworkId} 2 0x0300 0x06 {${hexHue} ${hexSat} ${intTo16bitUnsignedHex(rate / 100)}}", 400 | "delay ${rate + 400}", 401 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0000 {}" 402 | ] 403 | } else { 404 | cmd = [ 405 | "he cmd 0x${device.deviceNetworkId} 2 0x0300 0x06 {${hexHue} ${hexSat} 0x0100}", "delay 200", 406 | "he cmd 0x${device.deviceNetworkId} 2 0x0006 1 {}","delay 200", 407 | "he rattr 0x${device.deviceNetworkId} 2 0x0006 0 {}","delay 200", 408 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0000 {}" 409 | ] 410 | } 411 | state.lastHue = hexHue 412 | return cmd 413 | } 414 | 415 | def setSaturation(value) { 416 | def rate = 1000 417 | def cmd = [] 418 | def isOn = device.currentValue("switch") == "on" 419 | def hexSat = zigbee.convertToHexString(Math.round(value * 254 / 100).toInteger(),2) 420 | def hexHue = state.lastHue 421 | if (isOn){ 422 | cmd = [ 423 | "he cmd 0x${device.deviceNetworkId} 2 0x0300 0x06 {${hexHue} ${hexSat} ${intTo16bitUnsignedHex(rate / 100)}}", 424 | "delay ${rate + 400}", 425 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0001 {}" 426 | ] 427 | } else { 428 | cmd = [ 429 | "he cmd 0x${device.deviceNetworkId} 2 0x0300 0x06 {${hexHue} ${hexSat} 0x0100}", "delay 200", 430 | "he cmd 0x${device.deviceNetworkId} 2 0x0006 1 {}","delay 200", 431 | "he rattr 0x${device.deviceNetworkId} 2 0x0006 0 {}","delay 200", 432 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0001 {}" 433 | ] 434 | } 435 | state.lastSaturation = hexSat 436 | return cmd 437 | } 438 | 439 | def refresh() { 440 | if (logEnable) log.debug "refresh" 441 | return [ 442 | "he rattr 0x${device.deviceNetworkId} 1 0x0402 0x0000 {}","delay 200", //temp OK 443 | "he rattr 0x${device.deviceNetworkId} 1 0x0403 0x0000 {}","delay 200", // pressure 444 | "he rattr 0x${device.deviceNetworkId} 1 0x0405 0x0000 {}" ,"delay 200", //hum OK 445 | "he rattr 0x${device.deviceNetworkId} 2 0x0006 0 {}","delay 200", //light state 446 | "he rattr 0x${device.deviceNetworkId} 2 0x0008 0 {}","delay 200", //light level 447 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0000 {}","delay 200", //hue 448 | "he rattr 0x${device.deviceNetworkId} 2 0x0300 0x0001 {}" //sat 449 | ] 450 | 451 | } 452 | 453 | 454 | def intTo16bitUnsignedHex(value) { 455 | def hexStr = zigbee.convertToHexString(value.toInteger(),4) 456 | return new String(hexStr.substring(2, 4) + hexStr.substring(0, 2)) 457 | } 458 | 459 | def intTo8bitUnsignedHex(value) { 460 | return zigbee.convertToHexString(value.toInteger(), 2) 461 | } 462 | 463 | --------------------------------------------------------------------------------