mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-10 13:21:52 +00:00
Compare commits
32 Commits
MSA-2069-1
...
MSA-2113-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7515fab78 | ||
|
|
de5f0683d3 | ||
|
|
36e63133fc | ||
|
|
838c466312 | ||
|
|
97bfe61baa | ||
|
|
34df40d5b4 | ||
|
|
545be046f0 | ||
|
|
a5041e0fcb | ||
|
|
5beae1d9fc | ||
|
|
e5738978b0 | ||
|
|
07064eb8cc | ||
|
|
f519a2d828 | ||
|
|
4da49283bf | ||
|
|
7389edf795 | ||
|
|
23154372d2 | ||
|
|
1b0437c633 | ||
|
|
b3d9578140 | ||
|
|
7549979be5 | ||
|
|
771926c337 | ||
|
|
f647be3a62 | ||
|
|
d2f981fd34 | ||
|
|
2536c69083 | ||
|
|
a58dd2094d | ||
|
|
c549a5bed0 | ||
|
|
ef5fffc4bc | ||
|
|
c370e88a6b | ||
|
|
ce8c50c630 | ||
|
|
b069669c11 | ||
|
|
4d61d28b42 | ||
|
|
6b1e41198c | ||
|
|
f752a01906 | ||
|
|
45663ffb86 |
@@ -1,450 +0,0 @@
|
||||
/**
|
||||
* Copyright 2016 Eric Maycock
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Sonoff Wifi Switch
|
||||
*
|
||||
* Author: Eric Maycock (erocm123)
|
||||
* Date: 2016-06-02
|
||||
*/
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.XmlSlurper
|
||||
|
||||
metadata {
|
||||
definition (name: "Sonoff Wifi Switch", namespace: "erocm123", author: "Eric Maycock") {
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Configuration"
|
||||
capability "Health Check"
|
||||
|
||||
command "reboot"
|
||||
|
||||
attribute "needUpdate", "string"
|
||||
}
|
||||
|
||||
simulator {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "Once you change values on this page, the corner of the \"configuration\" icon will change orange until all configuration parameters are updated.", title: "Settings", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
generate_preferences(configuration_model())
|
||||
}
|
||||
|
||||
tiles (scale: 2){
|
||||
multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.on", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.off", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", backgroundColor:"#00a0dc", icon: "st.switches.switch.off", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", icon: "st.switches.switch.on", nextState:"turningOn"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("configure", "device.needUpdate", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "NO" , label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
state "YES", label:'', action:"configuration.configure", icon:"https://github.com/erocm123/SmartThingsPublic/raw/master/devicetypes/erocm123/qubino-flush-1d-relay.src/configure@2x.png"
|
||||
}
|
||||
standardTile("reboot", "device.reboot", decoration: "flat", height: 2, width: 2, inactiveLabel: false) {
|
||||
state "default", label:"Reboot", action:"reboot", icon:"", backgroundColor:"#ffffff"
|
||||
}
|
||||
valueTile("ip", "ip", width: 2, height: 1) {
|
||||
state "ip", label:'IP Address\r\n${currentValue}'
|
||||
}
|
||||
valueTile("uptime", "uptime", width: 2, height: 1) {
|
||||
state "uptime", label:'Uptime ${currentValue}'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch",
|
||||
"refresh","configure","reboot",
|
||||
"ip", "uptime"])
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "installed()"
|
||||
configure()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
logging("configure()", 1)
|
||||
def cmds = []
|
||||
cmds = update_needed_settings()
|
||||
if (cmds != []) cmds
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
logging("updated()", 1)
|
||||
def cmds = []
|
||||
cmds = update_needed_settings()
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID])
|
||||
sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true)
|
||||
if (cmds != []) response(cmds)
|
||||
}
|
||||
|
||||
private def logging(message, level) {
|
||||
if (logLevel != "0"){
|
||||
switch (logLevel) {
|
||||
case "1":
|
||||
if (level > 1)
|
||||
log.debug "$message"
|
||||
break
|
||||
case "99":
|
||||
log.debug "$message"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def parse(description) {
|
||||
//log.debug "Parsing: ${description}"
|
||||
def events = []
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
def body
|
||||
//log.debug "descMap: ${descMap}"
|
||||
|
||||
if (!state.mac || state.mac != descMap["mac"]) {
|
||||
log.debug "Mac address of device found ${descMap["mac"]}"
|
||||
updateDataValue("mac", descMap["mac"])
|
||||
}
|
||||
|
||||
if (state.mac != null && state.dni != state.mac) state.dni = setDeviceNetworkId(state.mac)
|
||||
if (descMap["body"]) body = new String(descMap["body"].decodeBase64())
|
||||
|
||||
if (body && body != "") {
|
||||
|
||||
if(body.startsWith("{") || body.startsWith("[")) {
|
||||
def slurper = new JsonSlurper()
|
||||
def result = slurper.parseText(body)
|
||||
|
||||
log.debug "result: ${result}"
|
||||
|
||||
if (result.containsKey("type")) {
|
||||
if (result.type == "configuration")
|
||||
events << update_current_properties(result)
|
||||
}
|
||||
if (result.containsKey("power")) {
|
||||
events << createEvent(name: "switch", value: result.power)
|
||||
}
|
||||
if (result.containsKey("uptime")) {
|
||||
events << createEvent(name: "uptime", value: result.uptime, displayed: false)
|
||||
}
|
||||
} else {
|
||||
//log.debug "Response is not JSON: $body"
|
||||
}
|
||||
}
|
||||
|
||||
if (!device.currentValue("ip") || (device.currentValue("ip") != getDataValue("ip"))) events << createEvent(name: 'ip', value: getDataValue("ip"))
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
description.split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
|
||||
if (nameAndValue.length == 2) map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
else map += [(nameAndValue[0].trim()):""]
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
def cmds = []
|
||||
cmds << getAction("/on")
|
||||
return cmds
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
def cmds = []
|
||||
cmds << getAction("/off")
|
||||
return cmds
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh()"
|
||||
def cmds = []
|
||||
cmds << getAction("/status")
|
||||
return cmds
|
||||
}
|
||||
|
||||
def ping() {
|
||||
log.debug "ping()"
|
||||
refresh()
|
||||
}
|
||||
|
||||
private getAction(uri){
|
||||
updateDNI()
|
||||
def userpass
|
||||
//log.debug uri
|
||||
if(password != null && password != "")
|
||||
userpass = encodeCredentials("admin", password)
|
||||
|
||||
def headers = getHeader(userpass)
|
||||
|
||||
def hubAction = new physicalgraph.device.HubAction(
|
||||
method: "GET",
|
||||
path: uri,
|
||||
headers: headers
|
||||
)
|
||||
return hubAction
|
||||
}
|
||||
|
||||
private postAction(uri, data){
|
||||
updateDNI()
|
||||
|
||||
def userpass
|
||||
|
||||
if(password != null && password != "")
|
||||
userpass = encodeCredentials("admin", password)
|
||||
|
||||
def headers = getHeader(userpass)
|
||||
|
||||
def hubAction = new physicalgraph.device.HubAction(
|
||||
method: "POST",
|
||||
path: uri,
|
||||
headers: headers,
|
||||
body: data
|
||||
)
|
||||
return hubAction
|
||||
}
|
||||
|
||||
private setDeviceNetworkId(ip, port = null){
|
||||
def myDNI
|
||||
if (port == null) {
|
||||
myDNI = ip
|
||||
} else {
|
||||
def iphex = convertIPtoHex(ip)
|
||||
def porthex = convertPortToHex(port)
|
||||
myDNI = "$iphex:$porthex"
|
||||
}
|
||||
log.debug "Device Network Id set to ${myDNI}"
|
||||
return myDNI
|
||||
}
|
||||
|
||||
private updateDNI() {
|
||||
if (state.dni != null && state.dni != "" && device.deviceNetworkId != state.dni) {
|
||||
device.deviceNetworkId = state.dni
|
||||
}
|
||||
}
|
||||
|
||||
private getHostAddress() {
|
||||
if (override == "true" && ip != null && ip != ""){
|
||||
return "${ip}:80"
|
||||
}
|
||||
else if(getDeviceDataByName("ip") && getDeviceDataByName("port")){
|
||||
return "${getDeviceDataByName("ip")}:${getDeviceDataByName("port")}"
|
||||
}else{
|
||||
return "${ip}:80"
|
||||
}
|
||||
}
|
||||
|
||||
private String convertIPtoHex(ipAddress) {
|
||||
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
|
||||
return hex
|
||||
}
|
||||
|
||||
private String convertPortToHex(port) {
|
||||
String hexport = port.toString().format( '%04x', port.toInteger() )
|
||||
return hexport
|
||||
}
|
||||
|
||||
private encodeCredentials(username, password){
|
||||
def userpassascii = "${username}:${password}"
|
||||
def userpass = "Basic " + userpassascii.encodeAsBase64().toString()
|
||||
return userpass
|
||||
}
|
||||
|
||||
private getHeader(userpass = null){
|
||||
def headers = [:]
|
||||
headers.put("Host", getHostAddress())
|
||||
headers.put("Content-Type", "application/x-www-form-urlencoded")
|
||||
if (userpass != null)
|
||||
headers.put("Authorization", userpass)
|
||||
return headers
|
||||
}
|
||||
|
||||
def reboot() {
|
||||
log.debug "reboot()"
|
||||
def uri = "/reboot"
|
||||
getAction(uri)
|
||||
}
|
||||
|
||||
def sync(ip, port) {
|
||||
def existingIp = getDataValue("ip")
|
||||
def existingPort = getDataValue("port")
|
||||
if (ip && ip != existingIp) {
|
||||
updateDataValue("ip", ip)
|
||||
sendEvent(name: 'ip', value: ip)
|
||||
}
|
||||
if (port && port != existingPort) {
|
||||
updateDataValue("port", port)
|
||||
}
|
||||
}
|
||||
|
||||
def generate_preferences(configuration_model)
|
||||
{
|
||||
def configuration = parseXml(configuration_model)
|
||||
|
||||
configuration.Value.each
|
||||
{
|
||||
if(it.@hidden != "true" && it.@disabled != "true"){
|
||||
switch(it.@type)
|
||||
{
|
||||
case ["number"]:
|
||||
input "${it.@index}", "number",
|
||||
title:"${it.@label}\n" + "${it.Help}",
|
||||
range: "${it.@min}..${it.@max}",
|
||||
defaultValue: "${it.@value}",
|
||||
displayDuringSetup: "${it.@displayDuringSetup}"
|
||||
break
|
||||
case "list":
|
||||
def items = []
|
||||
it.Item.each { items << ["${it.@value}":"${it.@label}"] }
|
||||
input "${it.@index}", "enum",
|
||||
title:"${it.@label}\n" + "${it.Help}",
|
||||
defaultValue: "${it.@value}",
|
||||
displayDuringSetup: "${it.@displayDuringSetup}",
|
||||
options: items
|
||||
break
|
||||
case ["password"]:
|
||||
input "${it.@index}", "password",
|
||||
title:"${it.@label}\n" + "${it.Help}",
|
||||
displayDuringSetup: "${it.@displayDuringSetup}"
|
||||
break
|
||||
case "decimal":
|
||||
input "${it.@index}", "decimal",
|
||||
title:"${it.@label}\n" + "${it.Help}",
|
||||
range: "${it.@min}..${it.@max}",
|
||||
defaultValue: "${it.@value}",
|
||||
displayDuringSetup: "${it.@displayDuringSetup}"
|
||||
break
|
||||
case "boolean":
|
||||
input "${it.@index}", "boolean",
|
||||
title:"${it.@label}\n" + "${it.Help}",
|
||||
defaultValue: "${it.@value}",
|
||||
displayDuringSetup: "${it.@displayDuringSetup}"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Code has elements from other community source @CyrilPeponnet (Z-Wave Parameter Sync). */
|
||||
|
||||
def update_current_properties(cmd)
|
||||
{
|
||||
def currentProperties = state.currentProperties ?: [:]
|
||||
currentProperties."${cmd.name}" = cmd.value
|
||||
|
||||
if (settings."${cmd.name}" != null)
|
||||
{
|
||||
if (settings."${cmd.name}".toString() == cmd.value)
|
||||
{
|
||||
sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: true)
|
||||
}
|
||||
else
|
||||
{
|
||||
sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
state.currentProperties = currentProperties
|
||||
}
|
||||
|
||||
|
||||
def update_needed_settings()
|
||||
{
|
||||
def cmds = []
|
||||
def currentProperties = state.currentProperties ?: [:]
|
||||
|
||||
def configuration = parseXml(configuration_model())
|
||||
def isUpdateNeeded = "NO"
|
||||
|
||||
cmds << getAction("/configSet?name=haip&value=${device.hub.getDataValue("localIP")}")
|
||||
cmds << getAction("/configSet?name=haport&value=${device.hub.getDataValue("localSrvPortTCP")}")
|
||||
|
||||
configuration.Value.each
|
||||
{
|
||||
if ("${it.@setting_type}" == "lan" && it.@disabled != "true"){
|
||||
if (currentProperties."${it.@index}" == null)
|
||||
{
|
||||
if (it.@setonly == "true"){
|
||||
logging("Setting ${it.@index} will be updated to ${it.@value}", 2)
|
||||
cmds << getAction("/configSet?name=${it.@index}&value=${it.@value}")
|
||||
} else {
|
||||
isUpdateNeeded = "YES"
|
||||
logging("Current value of setting ${it.@index} is unknown", 2)
|
||||
cmds << getAction("/configGet?name=${it.@index}")
|
||||
}
|
||||
}
|
||||
else if ((settings."${it.@index}" != null || it.@hidden == "true") && currentProperties."${it.@index}" != (settings."${it.@index}"? settings."${it.@index}".toString() : "${it.@value}"))
|
||||
{
|
||||
isUpdateNeeded = "YES"
|
||||
logging("Setting ${it.@index} will be updated to ${settings."${it.@index}"}", 2)
|
||||
cmds << getAction("/configSet?name=${it.@index}&value=${settings."${it.@index}"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true)
|
||||
return cmds
|
||||
}
|
||||
|
||||
def configuration_model()
|
||||
{
|
||||
'''
|
||||
<configuration>
|
||||
<Value type="password" byteSize="1" index="password" label="Password" min="" max="" value="" setting_type="preference" fw="">
|
||||
<Help>
|
||||
</Help>
|
||||
</Value>
|
||||
<Value type="list" byteSize="1" index="pos" label="Boot Up State" min="0" max="2" value="0" setting_type="lan" fw="">
|
||||
<Help>
|
||||
Default: Off
|
||||
</Help>
|
||||
<Item label="Off" value="0" />
|
||||
<Item label="On" value="1" />
|
||||
<Item label="Previous" value="2" />
|
||||
</Value>
|
||||
<Value type="number" byteSize="1" index="autooff" label="Auto Off" min="0" max="65536" value="0" setting_type="lan" fw="">
|
||||
<Help>
|
||||
Automatically turn the switch off after this many seconds.
|
||||
Range: 0 to 65536
|
||||
Default: 0 (Disabled)
|
||||
</Help>
|
||||
</Value>
|
||||
<Value type="list" byteSize="1" index="switchtype" label="External Switch Type" min="0" max="1" value="0" setting_type="lan" fw="">
|
||||
<Help>
|
||||
If a switch is attached to GPIO 14.
|
||||
Default: Momentary
|
||||
</Help>
|
||||
<Item label="Momentary" value="0" />
|
||||
<Item label="Toggle" value="1" />
|
||||
</Value>
|
||||
<Value type="list" index="logLevel" label="Debug Logging Level?" value="0" setting_type="preference" fw="">
|
||||
<Help>
|
||||
</Help>
|
||||
<Item label="None" value="0" />
|
||||
<Item label="Reports" value="1" />
|
||||
<Item label="All" value="99" />
|
||||
</Value>
|
||||
</configuration>
|
||||
'''
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* FortrezZ MIMO2+ B-Side
|
||||
*
|
||||
* Copyright 2016 FortrezZ, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "FortrezZ MIMO2+ B-Side", namespace: "fortrezz", author: "FortrezZ, LLC") {
|
||||
capability "Contact Sensor"
|
||||
capability "Relay Switch"
|
||||
capability "Switch"
|
||||
capability "Voltage Measurement"
|
||||
capability "Refresh"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2) {
|
||||
state "on", label: "Relay 2 On", action: "off", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0"
|
||||
state "off", label: "Relay 2 Off", action: "on", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("anaDig1", "device.anaDig1", inactiveLabel: false) {
|
||||
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
|
||||
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
|
||||
state "val", label:'${currentValue}v', unit:"", defaultState: true
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("powered", "device.powered", inactiveLabel: false) {
|
||||
state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||
state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
standardTile("blank", "device.blank", inactiveLabel: true, decoration: "flat") {
|
||||
state("blank", label: '')
|
||||
}
|
||||
main (["switch"])
|
||||
details(["switch", "anaDig1", "blank", "blank", "refresh", "powered"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// TODO: handle 'contact' attribute
|
||||
// TODO: handle 'switch' attribute
|
||||
// TODO: handle 'switch' attribute
|
||||
// TODO: handle 'voltage' attribute
|
||||
|
||||
}
|
||||
|
||||
def eventParse(evt) {
|
||||
log.debug("Event: ${evt.name}=${evt.value}")
|
||||
switch(evt.name) {
|
||||
case "powered":
|
||||
sendEvent(name: evt.name, value: evt.value)
|
||||
break
|
||||
case "switch2":
|
||||
sendEvent(name: "switch", value: evt.value)
|
||||
break
|
||||
case "contact2":
|
||||
sendEvent(name: "contact", value: evt.value)
|
||||
break
|
||||
case "voltage2":
|
||||
sendEvent(name: "voltage", value: evt.value)
|
||||
break
|
||||
case "relay2":
|
||||
sendEvent(name: evt.name, value: evt.value)
|
||||
break
|
||||
case "anaDig2":
|
||||
sendEvent(name: "anaDig1", value: evt.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on() {
|
||||
parent.on2(device.id)
|
||||
log.debug("Executing 'on'")
|
||||
// TODO: Send Event to parent device for "on2"
|
||||
}
|
||||
|
||||
def off() {
|
||||
parent.off2(device.id)
|
||||
log.debug("Executing 'off'")
|
||||
// TODO: Send Event to parent device for "off2"
|
||||
}
|
||||
def refresh() {
|
||||
parent.refresh2(device.id)
|
||||
log.debug("Executing 'refresh'")
|
||||
}
|
||||
379
devicetypes/fortrezz/fortrezz-mimo2.src/fortrezz-mimo2.groovy
Normal file
379
devicetypes/fortrezz/fortrezz-mimo2.src/fortrezz-mimo2.groovy
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* MIMO2 Device Handler
|
||||
*
|
||||
* Copyright 2016 FortrezZ, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "FortrezZ MIMO2+", namespace: "fortrezz", author: "FortrezZ, LLC") {
|
||||
capability "Alarm"
|
||||
capability "Contact Sensor"
|
||||
capability "Switch"
|
||||
capability "Voltage Measurement"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
|
||||
attribute "powered", "string"
|
||||
attribute "relay", "string"
|
||||
|
||||
attribute "relay2", "string"
|
||||
attribute "contact2", "string"
|
||||
attribute "voltage2", "string"
|
||||
|
||||
command "on"
|
||||
command "off"
|
||||
command "on2"
|
||||
command "off2"
|
||||
|
||||
fingerprint deviceId: "0x2100", inClusters: "0x5E,0x86,0x72,0x5A,0x59,0x71,0x98,0x7A"
|
||||
}
|
||||
|
||||
preferences {
|
||||
|
||||
input ("RelaySwitchDelay", "decimal", title: "Delay between relay switch on and off in seconds. Only Numbers 0 to 3 allowed. 0 value will remove delay and allow relay to function as a standard switch:\nRelay 1", description: "Numbers 0 to 3.1 allowed.", defaultValue: 0, required: false, displayDuringSetup: true)
|
||||
input ("RelaySwitchDelay2", "decimal", title: "Relay 2", description: "Numbers 0 to 3.1 allowed.", defaultValue: 0, required: false, displayDuringSetup: true)
|
||||
input ("Sig1AD", "bool", title: "Switch off for digital, on for analog:\nSIG1", required: false, displayDuringSetup: true)
|
||||
input ("Sig2AD", "bool", title: "SIG2", required: false, displayDuringSetup: true)
|
||||
} // the range would be 0 to 3.1, but the range value would not accept 3.1, only whole numbers (i tried paranthesis and fractions too. :( )
|
||||
|
||||
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2) {
|
||||
state "on", label: "Relay 1 On", action: "off", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0"
|
||||
state "off", label: "Relay 1 Off", action: "on", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff"
|
||||
|
||||
}
|
||||
standardTile("switch2", "device.switch2", width: 2, height: 2, inactiveLabel: false) {
|
||||
state "on", label: "Relay 2 On", action: "off2", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-On-104-edit.png", backgroundColor: "#53a7c0"
|
||||
state "off", label: 'Relay 2 Off', action: "on2", icon: "http://swiftlet.technology/wp-content/uploads/2016/06/Switch-Off-104-edit.png", backgroundColor: "#ffffff"
|
||||
}
|
||||
standardTile("anaDig1", "device.anaDig1", inactiveLabel: false) {
|
||||
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
|
||||
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
|
||||
state "val", label:'${currentValue}v', unit:"", defaultState: true
|
||||
}
|
||||
standardTile("anaDig2", "device.anaDig2", inactiveLabel: false) {
|
||||
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
|
||||
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
|
||||
state "val", label:'${currentValue}v', unit:"", defaultState: true
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("powered", "device.powered", inactiveLabel: false) {
|
||||
state "powerOn", label: "Power On", icon: "st.switches.switch.on", backgroundColor: "#79b821"
|
||||
state "powerOff", label: "Power Off", icon: "st.switches.switch.off", backgroundColor: "#ffa81e"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
standardTile("blank", "device.blank", inactiveLabel: true, decoration: "flat") {
|
||||
state("blank", label: '')
|
||||
}
|
||||
main (["switch"])
|
||||
details(["switch", "anaDig1", "blank", "switch2", "anaDig2", "blank", "configure", "refresh", "powered"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
def result = null
|
||||
def cmd = zwave.parse(description)
|
||||
|
||||
if (cmd.CMD == "7105") { //Mimo sent a power loss report
|
||||
log.debug "Device lost power"
|
||||
sendEvent(name: "powered", value: "powerOff", descriptionText: "$device.displayName lost power")
|
||||
} else {
|
||||
sendEvent(name: "powered", value: "powerOn", descriptionText: "$device.displayName regained power")
|
||||
}
|
||||
if (cmd) {
|
||||
def eventReturn = zwaveEvent(cmd)
|
||||
if(eventReturn in physicalgraph.device.HubMultiAction) {
|
||||
result = eventReturn
|
||||
}
|
||||
else {
|
||||
result = createEvent(eventReturn)
|
||||
}
|
||||
}
|
||||
log.debug "Parse returned ${result} $cmd.CMD"
|
||||
return result
|
||||
}
|
||||
|
||||
def updated() { // neat built-in smartThings function which automatically runs whenever any setting inputs are changed in the preferences menu of the device handler
|
||||
|
||||
if (state.count == 1) // this bit with state keeps the function from running twice ( which it always seems to want to do) (( oh, and state.count is a variable which is nonVolatile and doesn't change per every parse request.
|
||||
{
|
||||
state.count = 0
|
||||
log.debug "Settings Updated..."
|
||||
return response(delayBetween([
|
||||
configure(), // the response() function is used for sending commands in reponse to an event, without it, no zWave commands will work for contained function
|
||||
refresh()
|
||||
], 200))
|
||||
}
|
||||
else {state.count = 1}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) // basic set is essentially our digital sensor for SIG1 and SIG2 - it doesn't use an endpoint so we are having it send a multilevelGet() for SIG1 and SIG2 to see which one triggered.
|
||||
{
|
||||
log.debug "sent a BasicSet command"
|
||||
return response(refresh())
|
||||
}
|
||||
|
||||
def zwaveEvent(int endPoint, physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) // event to get the state of the digital sensor SIG1 and SIG2
|
||||
{
|
||||
log.debug "sent a sensorBinaryReport command"
|
||||
return response(refresh())
|
||||
}
|
||||
|
||||
def zwaveEvent(int endPoint, physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) // event for seeing the states of relay 1 and relay 2
|
||||
{
|
||||
def map = [:] // map for containing the name and state fo the specified relay
|
||||
if (endPoint == 3)
|
||||
{
|
||||
if (cmd.value) // possible values are 255 and 0 (0 is false)
|
||||
{map.value = "on"}
|
||||
else
|
||||
{map.value = "off"}
|
||||
map.name = "switch"
|
||||
log.debug "sent a SwitchBinary command $map.name $map.value" // the map is only used for debug messages. not for the return command to the device
|
||||
return [name: "switch", value: cmd.value ? "on" : "off"]
|
||||
}
|
||||
else if (endPoint == 4)
|
||||
{
|
||||
if (cmd.value)
|
||||
{map.value = "on"}
|
||||
else
|
||||
{map.value = "off"}
|
||||
map.name = "switch2"
|
||||
sendEvent(name: "relay2", value: "$map.value")
|
||||
log.debug "sent a SwitchBinary command $map.name $map.value" // the map is only used for debug messages. not for the return command to the device
|
||||
return [name: "switch2", value: cmd.value ? "on" : "off"]
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent (int endPoint, physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) // sensorMultilevelReport is used to report the value of the analog voltage for SIG1
|
||||
{
|
||||
def map = [:]
|
||||
def stdEvent = [:]
|
||||
def voltageVal = CalculateVoltage(cmd.scaledSensorValue) // saving the scaled Sensor Value used to enter into a large formula to determine actual voltage value
|
||||
if (endPoint == 1) //endPoint 1 is for SIG1
|
||||
{
|
||||
if (state.AD1 == false) // state.AD1 is to determine which state the anaDig1 tile should be in (either analogue or digital mode)
|
||||
{
|
||||
map.name = "anaDig1"
|
||||
stdEvent.name = "contact"
|
||||
if (voltageVal < 2) { // DK changed to 2v to follow LED behavior
|
||||
map.value = "closed"
|
||||
stdEvent.value = "closed"
|
||||
}
|
||||
else
|
||||
{
|
||||
map.value = "open"
|
||||
stdEvent.value = "open"
|
||||
}
|
||||
}
|
||||
else //or state.AD1 is true for analogue mode
|
||||
{
|
||||
map.name = "anaDig1"
|
||||
stdEvent.name = "voltage"
|
||||
map.value = voltageVal
|
||||
stdEvent.value = voltageVal
|
||||
map.unit = "v"
|
||||
stdEvent.unit = "v"
|
||||
}
|
||||
}
|
||||
else if (endPoint == 2) // endpoint 2 is for SIG2
|
||||
{
|
||||
if (state.AD2 == false)
|
||||
{
|
||||
map.name = "anaDig2"
|
||||
stdEvent.name = "contact2"
|
||||
if (voltageVal < 2) {
|
||||
map.value = "closed"
|
||||
stdEvent.value = "closed"
|
||||
}
|
||||
else
|
||||
{
|
||||
map.value = "open"
|
||||
stdEvent.value = "open"
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
map.name = "anaDig2"
|
||||
stdEvent.name = "voltage2"
|
||||
map.value = voltageVal
|
||||
stdEvent.value = voltageVal
|
||||
map.unit = "v"
|
||||
stdEvent.unit = "v"
|
||||
}
|
||||
}
|
||||
log.debug "sent a SensorMultilevelReport $map.name $map.value"
|
||||
sendEvent(stdEvent)
|
||||
return map
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { //standard security encapsulation event code (should be the same on all device handlers)
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand()
|
||||
// can specify command class versions here like in zwave.parse
|
||||
if (encapsulatedCommand) {
|
||||
return zwaveEvent(encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
// MultiChannelCmdEncap and MultiInstanceCmdEncap are ways that devices
|
||||
// can indicate that a message is coming from one of multiple subdevices
|
||||
// or "endpoints" that would otherwise be indistinguishable
|
||||
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand()
|
||||
log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}")
|
||||
|
||||
if (encapsulatedCommand) {
|
||||
return zwaveEvent(cmd.sourceEndPoint, encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(int endPoint, physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) {
|
||||
|
||||
log.debug "sent an Association Report"
|
||||
log.debug " ${cmd.groupingIdentifier}"
|
||||
//return [:]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
// Handles all Z-Wave commands we aren't interested in
|
||||
log.debug("Un-parsed Z-Wave message ${cmd}")
|
||||
return [:]
|
||||
}
|
||||
|
||||
def CalculateVoltage(ADCvalue) // used to calculate the voltage based on the collected Scaled sensor value of the multilevel sensor event
|
||||
{
|
||||
def volt = (((2.396*(10**-17))*(ADCvalue**5)) - ((1.817*(10**-13))*(ADCvalue**4)) + ((5.087*(10**-10))*(ADCvalue**3)) - ((5.868*(10**-7))*(ADCvalue**2)) + ((9.967*(10**-4))*(ADCvalue)) - (1.367*(10**-2)))
|
||||
return volt.round(1)
|
||||
}
|
||||
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring...."
|
||||
def sig1
|
||||
def sig2
|
||||
if (Sig1AD == true)
|
||||
{ sig1 = 0x01
|
||||
state.AD1 = true}
|
||||
else if (Sig1AD == false)
|
||||
{ sig1 = 0x40
|
||||
state.AD1 = false}
|
||||
if (Sig2AD == true)
|
||||
{ sig2 = 0x01
|
||||
state.AD2 = true}
|
||||
else if (Sig2AD == false)
|
||||
{ sig2 = 0x40
|
||||
state.AD2 = false}
|
||||
|
||||
def delay = (RelaySwitchDelay*10).toInteger() // the input which we get from the user is a string and is in seconds while the MIMO2 configuration requires it in 100ms so - change to integer and multiply by 10
|
||||
def delay2 = (RelaySwitchDelay2*10).toInteger() // the input which we get from the user is a string and is in seconds while the MIMO2 configuration requires it in 100ms so - change to integer and multiply by 10
|
||||
if (delay > 31)
|
||||
{
|
||||
log.debug "Relay 1 input ${delay / 10} set too high. Max value is 3.1"
|
||||
delay = 31
|
||||
}
|
||||
if (delay < 0)
|
||||
{
|
||||
log.debug "Relay 1 input ${delay / 10} set too low. Min value is 0"
|
||||
delay = 0
|
||||
}
|
||||
if (delay2 > 31)
|
||||
{
|
||||
log.debug "Relay 2 input ${delay2 / 10} set too high. Max value is 3.1"
|
||||
delay2 = 31
|
||||
}
|
||||
if (delay2 < 0)
|
||||
{
|
||||
log.debug "Relay 2 input ${delay2 / 10} set too low. Min value is 0"
|
||||
delay = 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
return delayBetween([
|
||||
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]), 0),
|
||||
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 0),
|
||||
|
||||
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 1),
|
||||
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]), 2),
|
||||
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]), 3),
|
||||
encap(zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]), 4),
|
||||
|
||||
secure(zwave.configurationV1.configurationSet(configurationValue: [sig1], parameterNumber: 3, size: 1)), // sends a multiLevelSensor report every 30 seconds for SIG1
|
||||
secure(zwave.configurationV1.configurationSet(configurationValue: [sig2], parameterNumber: 9, size: 1)), // sends a multiLevelSensor report every 30 seconds for SIG2
|
||||
secure(zwave.configurationV1.configurationSet(configurationValue: [delay], parameterNumber: 1, size: 1)), // configurationValue for parameterNumber means how many 100ms do you want the relay
|
||||
// to wait before it cycles again / size should just be 1 (for 1 byte.)
|
||||
secure(zwave.configurationV1.configurationSet(configurationValue: [delay2], parameterNumber: 2, size: 1)),
|
||||
|
||||
], 200)
|
||||
}
|
||||
|
||||
def on() {
|
||||
return encap(zwave.basicV1.basicSet(value: 0xff), 3) // physically changes the relay from on to off and requests a report of the relay
|
||||
// oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here.
|
||||
}
|
||||
|
||||
def off() {
|
||||
return encap(zwave.basicV1.basicSet(value: 0x00), 3) // physically changes the relay from on to off and requests a report of the relay
|
||||
// oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here.
|
||||
}
|
||||
|
||||
def on2() {
|
||||
return encap(zwave.basicV1.basicSet(value: 0xff), 4)
|
||||
// oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here.
|
||||
}
|
||||
|
||||
def off2() {
|
||||
return encap(zwave.basicV1.basicSet(value: 0x00), 4)
|
||||
// oddly, smartThings automatically sends a switchBinaryGet() command whenever the above basicSet command is sent, so we don't need to send one here.
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refresh"
|
||||
return delayBetween([
|
||||
encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 1),// requests a report of the anologue input voltage for SIG1
|
||||
encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 2),// requests a report of the anologue input voltage for SIG2
|
||||
encap(zwave.switchBinaryV1.switchBinaryGet(), 3), //requests a report of the relay to make sure that it changed for Relay 1
|
||||
encap(zwave.switchBinaryV1.switchBinaryGet(), 4), //requests a report of the relay to make sure that it changed for Relay 2
|
||||
],200)
|
||||
}
|
||||
|
||||
def refreshZWave() {
|
||||
log.debug "Refresh (Z-Wave Response)"
|
||||
return delayBetween([
|
||||
encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 1),// requests a report of the anologue input voltage for SIG1
|
||||
encap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 2),// requests a report of the anologue input voltage for SIG2
|
||||
encap(zwave.switchBinaryV1.switchBinaryGet(), 3), //requests a report of the relay to make sure that it changed for Relay 1
|
||||
encap(zwave.switchBinaryV1.switchBinaryGet(), 4) //requests a report of the relay to make sure that it changed for Relay 2
|
||||
],200)
|
||||
}
|
||||
|
||||
private secureSequence(commands, delay=200) { // decided not to use this
|
||||
return delayBetween(commands.collect{ secure(it) }, delay)
|
||||
}
|
||||
|
||||
private secure(physicalgraph.zwave.Command cmd) { //take multiChannel message and securely encrypts the message so the device can read it
|
||||
return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
}
|
||||
|
||||
private encap(cmd, endpoint) { // takes desired command and encapsulates it by multiChannel and then sends it to secure() to be wrapped with another encapsulation for secure encryption
|
||||
if (endpoint) {
|
||||
return secure(zwave.multiChannelV3.multiChannelCmdEncap(bitAddress: false, sourceEndPoint:0, destinationEndPoint: endpoint).encapsulate(cmd))
|
||||
} else {
|
||||
return secure(cmd)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Spruce Sensor -Pre-release V2 10/8/2015
|
||||
* Spruce Sensor -updated with SLP model number 5/2017
|
||||
*
|
||||
* Copyright 2014 Plaid Systems
|
||||
*
|
||||
@@ -14,25 +14,33 @@
|
||||
*
|
||||
-------10/20/2015 Updates--------
|
||||
-Fix/add battery reporting interval to update
|
||||
-remove polling and/or refresh(?)
|
||||
-remove polling and/or refresh
|
||||
|
||||
-------5/2017 Updates--------
|
||||
-Add fingerprints for SLP
|
||||
-add device health, check every 60mins + 2mins
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "NCauffman") {
|
||||
definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "Plaid Systems") {
|
||||
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Temperature Measurement"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
//capability "Polling"
|
||||
|
||||
attribute "maxHum", "string"
|
||||
attribute "minHum", "string"
|
||||
|
||||
|
||||
command "resetHumidity"
|
||||
command "refresh"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01", deviceJoinName: "Spruce Sensor"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-SLP1", deviceJoinName: "Spruce Sensor"
|
||||
}
|
||||
|
||||
preferences {
|
||||
@@ -293,6 +301,11 @@ def setConfig(){
|
||||
sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration initialized")
|
||||
}
|
||||
|
||||
def installed(){
|
||||
//check every 1 hour + 2mins
|
||||
sendEvent(name: "checkInterval", value: 1 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
//when device preferences are changed
|
||||
def updated(){
|
||||
log.debug "device updated"
|
||||
@@ -303,6 +316,8 @@ def updated(){
|
||||
sendEvent(name: 'configuration',value: 0, descriptionText: "Settings changed and will update at next report. Measure interval set to ${interval} mins")
|
||||
}
|
||||
}
|
||||
//check every 1 hour + 2mins
|
||||
sendEvent(name: "checkInterval", value: 1 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
//poll
|
||||
@@ -395,4 +410,4 @@ private byte[] reverseArray(byte[] array) {
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,9 @@ Works with:
|
||||
## Device Health
|
||||
|
||||
Aeon Labs MultiSensor 6 is polled by the hub.
|
||||
As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed.
|
||||
Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins.
|
||||
Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for
|
||||
the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row,
|
||||
it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time.
|
||||
Aeon MultiSensor reports in once every hour.
|
||||
|
||||
* __32min__ checkInterval
|
||||
* __122min__ checkInterval
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -130,13 +130,13 @@ metadata {
|
||||
}
|
||||
|
||||
def installed(){
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
// Device-Watch simply pings if no device events received for 122min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def updated() {
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
// Device-Watch simply pings if no device events received for 122min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}"
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ def generateEvent(Map results) {
|
||||
if(results) {
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
def supportedThermostatModes = []
|
||||
def supportedThermostatModes = ["off"]
|
||||
def thermostatMode = null
|
||||
|
||||
results.each { name, value ->
|
||||
|
||||
@@ -105,6 +105,8 @@ def parse(String description) {
|
||||
} else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}"
|
||||
}
|
||||
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
|
||||
map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
|
||||
}
|
||||
}
|
||||
} else if (map.name == "temperature") {
|
||||
@@ -129,6 +131,10 @@ def parse(String description) {
|
||||
private Map parseIasMessage(String description) {
|
||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||
|
||||
translateZoneStatus(zs)
|
||||
}
|
||||
|
||||
private Map translateZoneStatus(ZoneStatus zs) {
|
||||
return zs.isAlarm1Set() ? getMoistureResult('wet') : getMoistureResult('dry')
|
||||
}
|
||||
|
||||
@@ -197,7 +203,8 @@ def ping() {
|
||||
def refresh() {
|
||||
log.debug "Refreshing Temperature and Battery"
|
||||
def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
|
||||
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020)
|
||||
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
|
||||
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS)
|
||||
|
||||
return refreshCmds + zigbee.enrollResponse()
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ def parse(String description) {
|
||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||
log.debug "Doing a read attr motion event"
|
||||
map = getMotionResult(value)
|
||||
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
|
||||
map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
|
||||
}
|
||||
}
|
||||
} else if (map.name == "temperature") {
|
||||
@@ -135,6 +137,10 @@ def parse(String description) {
|
||||
private Map parseIasMessage(String description) {
|
||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||
|
||||
translateZoneStatus(zs)
|
||||
}
|
||||
|
||||
private Map translateZoneStatus(ZoneStatus zs) {
|
||||
// Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion
|
||||
return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive')
|
||||
}
|
||||
@@ -165,8 +171,8 @@ private Map getBatteryResult(rawValue) {
|
||||
def pct = batteryMap[volts]
|
||||
result.value = pct
|
||||
} else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def minVolts = 2.4
|
||||
def maxVolts = 2.7
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
@@ -200,7 +206,8 @@ def refresh() {
|
||||
log.debug "refresh called"
|
||||
|
||||
def refreshCmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
|
||||
zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000)
|
||||
zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
|
||||
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS)
|
||||
|
||||
return refreshCmds + zigbee.enrollResponse()
|
||||
}
|
||||
|
||||
@@ -134,8 +134,9 @@ def parse(String description) {
|
||||
} else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}"
|
||||
}
|
||||
} else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) {
|
||||
maps += translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value)))
|
||||
} else {
|
||||
|
||||
maps += handleAcceleration(descMap)
|
||||
}
|
||||
}
|
||||
@@ -229,6 +230,11 @@ private List<Map> parseAxis(List<Map> attrData) {
|
||||
|
||||
private List<Map> parseIasMessage(String description) {
|
||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||
|
||||
translateZoneStatus(zs)
|
||||
}
|
||||
|
||||
private List<Map> translateZoneStatus(ZoneStatus zs) {
|
||||
List<Map> results = []
|
||||
|
||||
if (garageSensor != "Yes") {
|
||||
@@ -268,7 +274,7 @@ private Map getBatteryResult(rawValue) {
|
||||
result.value = pct
|
||||
} else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def maxVolts = 2.7
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
@@ -313,7 +319,7 @@ def refresh() {
|
||||
def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +
|
||||
zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) +
|
||||
zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode]) +
|
||||
zigbee.enrollResponse()
|
||||
zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + zigbee.enrollResponse()
|
||||
|
||||
return refreshCmds
|
||||
}
|
||||
|
||||
@@ -28,9 +28,8 @@ Works with:
|
||||
|
||||
## Device Health
|
||||
|
||||
SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE`
|
||||
ZigBee Button is marked offline only in the case when Hub is offline.
|
||||
|
||||
* __722min__ checkInterval
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import physicalgraph.zigbee.zcl.DataType
|
||||
|
||||
metadata {
|
||||
@@ -183,13 +185,6 @@ private Map parseNonIasButtonMessage(Map descMap){
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
refresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing Battery"
|
||||
|
||||
@@ -198,8 +193,6 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 2 check-in misses from device (plus 2 mins lag time)
|
||||
sendEvent(name: "checkInterval", value: 2 * 6 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def cmds = []
|
||||
if (device.getDataValue("model") == "3450-L") {
|
||||
@@ -259,6 +252,8 @@ def updated() {
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// Arrival sensors only goes OFFLINE when Hub is off
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false)
|
||||
if ((device.getDataValue("manufacturer") == "OSRAM") && (device.getDataValue("model") == "LIGHTIFY Dimming Switch")) {
|
||||
sendEvent(name: "numberOfButtons", value: 2)
|
||||
}
|
||||
|
||||
@@ -15,10 +15,9 @@ metadata {
|
||||
definition (name: "Z-Wave Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Temperature Measurement"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Thermostat"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
@@ -117,7 +116,7 @@ metadata {
|
||||
state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"polling.poll", icon:"st.secondary.refresh"
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "temperature"
|
||||
details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh"])
|
||||
@@ -125,13 +124,20 @@ metadata {
|
||||
}
|
||||
|
||||
def installed(){
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()))
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated(){
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
unschedule()
|
||||
runEvery5Minutes("refresh")
|
||||
refresh()
|
||||
}
|
||||
|
||||
def parse(String description)
|
||||
@@ -149,6 +155,7 @@ def parse(String description)
|
||||
]
|
||||
if (map.name == "thermostatMode") {
|
||||
state.lastTriedMode = map.value
|
||||
map.data = [supportedThermostatModes:state.supportedThermostatModes]
|
||||
if (map.value == "cool") {
|
||||
map2.value = device.latestValue("coolingSetpoint")
|
||||
log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}"
|
||||
@@ -172,6 +179,7 @@ def parse(String description)
|
||||
}
|
||||
} else if (map.name == "thermostatFanMode" && map.isStateChange) {
|
||||
state.lastTriedFanMode = map.value
|
||||
map.data = [supportedThermostatFanModes: state.supportedThermostatFanModes]
|
||||
}
|
||||
log.debug "Parse returned $result"
|
||||
result
|
||||
@@ -305,26 +313,26 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanMod
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) {
|
||||
def supportedModes = ""
|
||||
if(cmd.off) { supportedModes += "off " }
|
||||
if(cmd.heat) { supportedModes += "heat " }
|
||||
if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " }
|
||||
if(cmd.cool) { supportedModes += "cool " }
|
||||
if(cmd.auto) { supportedModes += "auto " }
|
||||
def supportedModes = []
|
||||
if(cmd.off) { supportedModes << "off" }
|
||||
if(cmd.heat) { supportedModes << "heat" }
|
||||
if(cmd.cool) { supportedModes << "cool" }
|
||||
if(cmd.auto) { supportedModes << "auto" }
|
||||
if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" }
|
||||
|
||||
state.supportedModes = supportedModes
|
||||
// No events to be generated, return empty map
|
||||
state.supportedThermostatModes = supportedModes
|
||||
sendEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false)
|
||||
return [:]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) {
|
||||
def supportedFanModes = ""
|
||||
if(cmd.auto) { supportedFanModes += "auto " } // "fanAuto "
|
||||
if(cmd.low) { supportedFanModes += "on " } // "fanOn"
|
||||
if(cmd.circulation) { supportedFanModes += "circulate " } // "fanCirculate"
|
||||
def supportedFanModes = []
|
||||
if(cmd.auto) { supportedFanModes << "auto" } // "fanAuto "
|
||||
if(cmd.circulation) { supportedFanModes << "circulate" } // "fanCirculate"
|
||||
if(cmd.low) { supportedFanModes << "on" } // "fanOn"
|
||||
|
||||
state.supportedFanModes = supportedFanModes
|
||||
// No events to be generated, return empty map
|
||||
state.supportedThermostatFanModes = supportedFanModes
|
||||
sendEvent(name: "supportedThermostatFanModes", value: supportedFanModes, displayed: false)
|
||||
return [:]
|
||||
}
|
||||
|
||||
@@ -337,15 +345,17 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
}
|
||||
|
||||
// Command Implementations
|
||||
def poll() {
|
||||
delayBetween([
|
||||
zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
|
||||
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
|
||||
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
|
||||
zwave.thermostatModeV2.thermostatModeGet().format(),
|
||||
zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
|
||||
zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
|
||||
], 2300)
|
||||
def refresh() {
|
||||
def cmds = []
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.sensorMultilevelV2.sensorMultilevelGet().format()) // current temperature
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format())
|
||||
cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format())
|
||||
sendHubCommand(cmds)
|
||||
}
|
||||
|
||||
def quickSetHeat(degrees) {
|
||||
@@ -416,28 +426,14 @@ def ping() {
|
||||
poll()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
delayBetween([
|
||||
zwave.thermostatModeV2.thermostatModeSupportedGet().format(),
|
||||
zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(),
|
||||
zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
|
||||
zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature
|
||||
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
|
||||
zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(),
|
||||
zwave.thermostatModeV2.thermostatModeGet().format(),
|
||||
zwave.thermostatFanModeV3.thermostatFanModeGet().format(),
|
||||
zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()
|
||||
], 2300)
|
||||
}
|
||||
|
||||
def modes() {
|
||||
["off", "heat", "cool", "auto", "emergency heat"]
|
||||
return state.supportedThermostatModes
|
||||
}
|
||||
|
||||
def switchMode() {
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off"
|
||||
def supportedModes = getDataByName("supportedModes")
|
||||
def lastTriedMode = state.lastTriedMode ?: currentMode ?: ["off"]
|
||||
def supportedModes = getDataByName("supportedThermostatModes")
|
||||
def modeOrder = modes()
|
||||
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
|
||||
def nextMode = next(lastTriedMode)
|
||||
@@ -454,7 +450,7 @@ def switchMode() {
|
||||
}
|
||||
|
||||
def switchToMode(nextMode) {
|
||||
def supportedModes = getDataByName("supportedModes")
|
||||
def supportedModes = getDataByName("supportedThermostatModes")
|
||||
if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
|
||||
if (nextMode in modes()) {
|
||||
state.lastTriedMode = nextMode
|
||||
@@ -466,9 +462,9 @@ def switchToMode(nextMode) {
|
||||
|
||||
def switchFanMode() {
|
||||
def currentMode = device.currentState("thermostatFanMode")?.value
|
||||
def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off"
|
||||
def supportedModes = getDataByName("supportedFanModes") ?: "auto on" // "fanAuto fanOn"
|
||||
def modeOrder = ["auto", "circulate", "on"] // "fanAuto", "fanCirculate", "fanOn"
|
||||
def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: ["off"]
|
||||
def supportedModes = getDataByName("supportedThermostatFanModes") ?: ["auto", "on"]
|
||||
def modeOrder = state.supportedThermostatFanModes
|
||||
def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
|
||||
def nextMode = next(lastTriedMode)
|
||||
while (!supportedModes?.contains(nextMode) && nextMode != "auto") { // "fanAuto"
|
||||
@@ -478,7 +474,7 @@ def switchFanMode() {
|
||||
}
|
||||
|
||||
def switchToFanMode(nextMode) {
|
||||
def supportedFanModes = getDataByName("supportedFanModes")
|
||||
def supportedFanModes = getDataByName("supportedThermostatFanModes")
|
||||
if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported"
|
||||
|
||||
def returnCommand
|
||||
|
||||
@@ -58,6 +58,7 @@ metadata {
|
||||
def installed() {
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
response(refresh())
|
||||
}
|
||||
|
||||
def updated() {
|
||||
|
||||
@@ -1,409 +0,0 @@
|
||||
/**
|
||||
* Copyright 2016 Eric Maycock
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Sonoff (Connect)
|
||||
*
|
||||
* Author: Eric Maycock (erocm123)
|
||||
* Date: 2016-06-02
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Sonoff (Connect)",
|
||||
namespace: "erocm123",
|
||||
author: "Eric Maycock (erocm123)",
|
||||
description: "Service Manager for Sonoff switches",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://raw.githubusercontent.com/erocm123/SmartThingsPublic/master/smartapps/erocm123/sonoff-connect.src/sonoff-connect-icon.png",
|
||||
iconX2Url: "https://raw.githubusercontent.com/erocm123/SmartThingsPublic/master/smartapps/erocm123/sonoff-connect.src/sonoff-connect-icon-2x.png",
|
||||
iconX3Url: "https://raw.githubusercontent.com/erocm123/SmartThingsPublic/master/smartapps/erocm123/sonoff-connect.src/sonoff-connect-icon-3x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage")
|
||||
page(name: "configurePDevice")
|
||||
page(name: "deletePDevice")
|
||||
page(name: "changeName")
|
||||
page(name: "discoveryPage", title: "Device Discovery", content: "discoveryPage", refreshTimeout:5)
|
||||
page(name: "addDevices", title: "Add Sonoff Switches", content: "addDevices")
|
||||
page(name: "manuallyAdd")
|
||||
page(name: "manuallyAddConfirm")
|
||||
page(name: "deviceDiscovery")
|
||||
}
|
||||
|
||||
def mainPage() {
|
||||
dynamicPage(name: "mainPage", title: "Manage your Sonoff switches", nextPage: null, uninstall: true, install: true) {
|
||||
section("Configure"){
|
||||
href "deviceDiscovery", title:"Discover Devices", description:""
|
||||
href "manuallyAdd", title:"Manually Add Device", description:""
|
||||
}
|
||||
section("Installed Devices"){
|
||||
getChildDevices().sort({ a, b -> a["deviceNetworkId"] <=> b["deviceNetworkId"] }).each {
|
||||
href "configurePDevice", title:"$it.label", description:"", params: [did: it.deviceNetworkId]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def configurePDevice(params){
|
||||
if (params?.did || params?.params?.did) {
|
||||
if (params.did) {
|
||||
state.currentDeviceId = params.did
|
||||
state.currentDisplayName = getChildDevice(params.did)?.displayName
|
||||
} else {
|
||||
state.currentDeviceId = params.params.did
|
||||
state.currentDisplayName = getChildDevice(params.params.did)?.displayName
|
||||
}
|
||||
}
|
||||
if (getChildDevice(state.currentDeviceId) != null) getChildDevice(state.currentDeviceId).configure()
|
||||
dynamicPage(name: "configurePDevice", title: "Configure Sonoff Switches created with this app", nextPage: null) {
|
||||
section {
|
||||
app.updateSetting("${state.currentDeviceId}_label", getChildDevice(state.currentDeviceId).label)
|
||||
input "${state.currentDeviceId}_label", "text", title:"Device Name", description: "", required: false
|
||||
href "changeName", title:"Change Device Name", description: "Edit the name above and click here to change it"
|
||||
}
|
||||
section {
|
||||
href "deletePDevice", title:"Delete $state.currentDisplayName", description: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def manuallyAdd(){
|
||||
dynamicPage(name: "manuallyAdd", title: "Manually add a Sonoff device", nextPage: "manuallyAddConfirm") {
|
||||
section {
|
||||
paragraph "This process will manually create a Sonoff device based on the entered IP address. The SmartApp needs to then communicate with the device to obtain additional information from it. Make sure the device is on and connected to your wifi network."
|
||||
input "deviceType", "enum", title:"Device Type", description: "", required: false, options: ["Sonoff Wifi Switch","Sonoff TH Wifi Switch","Sonoff POW Wifi Switch","Sonoff Dual Wifi Switch","Sonoff 4CH Wifi Switch"]
|
||||
input "ipAddress", "text", title:"IP Address", description: "", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def manuallyAddConfirm(){
|
||||
if ( ipAddress =~ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/) {
|
||||
log.debug "Creating Sonoff Wifi Switch with dni: ${convertIPtoHex(ipAddress)}:${convertPortToHex("80")}"
|
||||
addChildDevice("erocm123", deviceType ? deviceType : "Sonoff Wifi Switch", "${convertIPtoHex(ipAddress)}:${convertPortToHex("80")}", location.hubs[0].id, [
|
||||
"label": (deviceType ? deviceType : "Sonoff Wifi Switch") + " (${ipAddress})",
|
||||
"data": [
|
||||
"ip": ipAddress,
|
||||
"port": "80"
|
||||
]
|
||||
])
|
||||
|
||||
app.updateSetting("ipAddress", "")
|
||||
|
||||
dynamicPage(name: "manuallyAddConfirm", title: "Manually add a Sonoff device", nextPage: "mainPage") {
|
||||
section {
|
||||
paragraph "The device has been added. Press next to return to the main page."
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dynamicPage(name: "manuallyAddConfirm", title: "Manually add a Sonoff device", nextPage: "mainPage") {
|
||||
section {
|
||||
paragraph "The entered ip address is not valid. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def deletePDevice(){
|
||||
try {
|
||||
unsubscribe()
|
||||
deleteChildDevice(state.currentDeviceId)
|
||||
dynamicPage(name: "deletePDevice", title: "Deletion Summary", nextPage: "mainPage") {
|
||||
section {
|
||||
paragraph "The device has been deleted. Press next to continue"
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
dynamicPage(name: "deletePDevice", title: "Deletion Summary", nextPage: "mainPage") {
|
||||
section {
|
||||
paragraph "Error: ${(e as String).split(":")[1]}."
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def changeName(){
|
||||
def thisDevice = getChildDevice(state.currentDeviceId)
|
||||
thisDevice.label = settings["${state.currentDeviceId}_label"]
|
||||
|
||||
dynamicPage(name: "changeName", title: "Change Name Summary", nextPage: "mainPage") {
|
||||
section {
|
||||
paragraph "The device has been renamed. Press \"Next\" to continue"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def discoveryPage(){
|
||||
return deviceDiscovery()
|
||||
}
|
||||
|
||||
def deviceDiscovery(params=[:])
|
||||
{
|
||||
def devices = devicesDiscovered()
|
||||
|
||||
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
|
||||
state.deviceRefreshCount = deviceRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def options = devices ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if ((numFound == 0 && state.deviceRefreshCount > 25) || params.reset == "true") {
|
||||
log.trace "Cleaning old device memory"
|
||||
state.devices = [:]
|
||||
state.deviceRefreshCount = 0
|
||||
app.updateSetting("selectedDevice", "")
|
||||
}
|
||||
|
||||
ssdpSubscribe()
|
||||
|
||||
//sonoff discovery request every 15 //25 seconds
|
||||
if((deviceRefreshCount % 5) == 0) {
|
||||
discoverDevices()
|
||||
}
|
||||
|
||||
//setup.xml request every 3 seconds except on discoveries
|
||||
if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 5) != 0)) {
|
||||
verifyDevices()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"addDevices", refreshInterval:refreshInterval, uninstall: true) {
|
||||
section("Please wait while we discover your Sonoff devices. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedDevices", "enum", required:false, title:"Select Sonoff Switch (${numFound} found)", multiple:true, options:options
|
||||
}
|
||||
section("Options") {
|
||||
href "deviceDiscovery", title:"Reset list of discovered devices", description:"", params: ["reset": "true"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map devicesDiscovered() {
|
||||
def vdevices = getVerifiedDevices()
|
||||
def map = [:]
|
||||
vdevices.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = "${it.value.mac}"
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
def getVerifiedDevices() {
|
||||
getDevices().findAll{ it?.value?.verified == true }
|
||||
}
|
||||
|
||||
private discoverDevices() {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:Basic:1", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
def configured() {
|
||||
|
||||
}
|
||||
|
||||
def buttonConfigured(idx) {
|
||||
return settings["lights_$idx"]
|
||||
}
|
||||
|
||||
def isConfigured(){
|
||||
if(getChildDevices().size() > 0) return true else return false
|
||||
}
|
||||
|
||||
def isVirtualConfigured(did){
|
||||
def foundDevice = false
|
||||
getChildDevices().each {
|
||||
if(it.deviceNetworkId != null){
|
||||
if(it.deviceNetworkId.startsWith("${did}/")) foundDevice = true
|
||||
}
|
||||
}
|
||||
return foundDevice
|
||||
}
|
||||
|
||||
private virtualCreated(number) {
|
||||
if (getChildDevice(getDeviceID(number))) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private getDeviceID(number) {
|
||||
return "${state.currentDeviceId}/${app.id}/${number}"
|
||||
}
|
||||
|
||||
def installed() {
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
ssdpSubscribe()
|
||||
runEvery5Minutes("ssdpDiscover")
|
||||
}
|
||||
|
||||
void ssdpSubscribe() {
|
||||
subscribe(location, "ssdpTerm.urn:schemas-upnp-org:device:Basic:1", ssdpHandler)
|
||||
}
|
||||
|
||||
void ssdpDiscover() {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:Basic:1", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
def ssdpHandler(evt) {
|
||||
def description = evt.description
|
||||
def hub = evt?.hubId
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
parsedEvent << ["hub":hub]
|
||||
|
||||
def devices = getDevices()
|
||||
|
||||
String ssdpUSN = parsedEvent.ssdpUSN.toString()
|
||||
|
||||
if (devices."${ssdpUSN}") {
|
||||
def d = devices."${ssdpUSN}"
|
||||
def child = getChildDevice(parsedEvent.mac)
|
||||
def childIP
|
||||
def childPort
|
||||
if (child) {
|
||||
childIP = child.getDeviceDataByName("ip")
|
||||
childPort = child.getDeviceDataByName("port").toString()
|
||||
log.debug "Device data: ($childIP:$childPort) - reporting data: (${convertHexToIP(parsedEvent.networkAddress)}:${convertHexToInt(parsedEvent.deviceAddress)})."
|
||||
if(childIP != convertHexToIP(parsedEvent.networkAddress) || childPort != convertHexToInt(parsedEvent.deviceAddress).toString()){
|
||||
log.debug "Device data (${child.getDeviceDataByName("ip")}) does not match what it is reporting(${convertHexToIP(parsedEvent.networkAddress)}). Attempting to update."
|
||||
child.sync(convertHexToIP(parsedEvent.networkAddress), convertHexToInt(parsedEvent.deviceAddress).toString())
|
||||
}
|
||||
}
|
||||
|
||||
if (d.networkAddress != parsedEvent.networkAddress || d.deviceAddress != parsedEvent.deviceAddress) {
|
||||
d.networkAddress = parsedEvent.networkAddress
|
||||
d.deviceAddress = parsedEvent.deviceAddress
|
||||
}
|
||||
} else {
|
||||
devices << ["${ssdpUSN}": parsedEvent]
|
||||
}
|
||||
}
|
||||
|
||||
void verifyDevices() {
|
||||
def devices = getDevices().findAll { it?.value?.verified != true }
|
||||
devices.each {
|
||||
def ip = convertHexToIP(it.value.networkAddress)
|
||||
def port = convertHexToInt(it.value.deviceAddress)
|
||||
String host = "${ip}:${port}"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${it.value.ssdpPath} HTTP/1.1\r\nHOST: $host\r\n\r\n""", physicalgraph.device.Protocol.LAN, host, [callback: deviceDescriptionHandler]))
|
||||
}
|
||||
}
|
||||
|
||||
def getDevices() {
|
||||
state.devices = state.devices ?: [:]
|
||||
}
|
||||
|
||||
void deviceDescriptionHandler(physicalgraph.device.HubResponse hubResponse) {
|
||||
log.trace "description.xml response (application/xml)"
|
||||
def body = hubResponse.xml
|
||||
log.debug body?.device?.friendlyName?.text()
|
||||
if (body?.device?.modelName?.text().startsWith("Sonoff")) {
|
||||
def devices = getDevices()
|
||||
def device = devices.find {it?.key?.contains(body?.device?.UDN?.text())}
|
||||
if (device) {
|
||||
device.value << [name:body?.device?.friendlyName?.text() + " (" + convertHexToIP(hubResponse.ip) + ")", serialNumber:body?.device?.serialNumber?.text(), verified: true]
|
||||
} else {
|
||||
log.error "/description.xml returned a device that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def addDevices() {
|
||||
def devices = getDevices()
|
||||
def sectionText = ""
|
||||
|
||||
selectedDevices.each { dni ->bridgeLinking
|
||||
def selectedDevice = devices.find { it.value.mac == dni }
|
||||
def d
|
||||
if (selectedDevice) {
|
||||
d = getChildDevices()?.find {
|
||||
it.deviceNetworkId == selectedDevice.value.mac
|
||||
}
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
log.debug selectedDevice
|
||||
log.debug "Creating Sonoff Switch with dni: ${selectedDevice.value.mac}"
|
||||
|
||||
def deviceHandlerName
|
||||
if (selectedDevice?.value?.name?.startsWith("Sonoff TH"))
|
||||
deviceHandlerName = "Sonoff TH Wifi Switch"
|
||||
else if (selectedDevice?.value?.name?.startsWith("Sonoff POW"))
|
||||
deviceHandlerName = "Sonoff POW Wifi Switch"
|
||||
else if (selectedDevice?.value?.name?.startsWith("Sonoff Dual"))
|
||||
deviceHandlerName = "Sonoff Dual Wifi Switch"
|
||||
else if (selectedDevice?.value?.name?.startsWith("Sonoff 4CH"))
|
||||
deviceHandlerName = "Sonoff 4CH Wifi Switch"
|
||||
else
|
||||
deviceHandlerName = "Sonoff Wifi Switch"
|
||||
def newDevice = addChildDevice("erocm123", deviceHandlerName, selectedDevice.value.mac, selectedDevice?.value.hub, [
|
||||
"label": selectedDevice?.value?.name ?: "Sonoff Wifi Switch",
|
||||
"data": [
|
||||
"mac": selectedDevice.value.mac,
|
||||
"ip": convertHexToIP(selectedDevice.value.networkAddress),
|
||||
"port": "" + Integer.parseInt(selectedDevice.value.deviceAddress,16)
|
||||
]
|
||||
])
|
||||
sectionText = sectionText + "Succesfully added Sonoff device with ip address ${convertHexToIP(selectedDevice.value.networkAddress)} \r\n"
|
||||
}
|
||||
|
||||
}
|
||||
log.debug sectionText
|
||||
return dynamicPage(name:"addDevices", title:"Devices Added", nextPage:"mainPage", uninstall: true) {
|
||||
if(sectionText != ""){
|
||||
section("Add Sonoff Results:") {
|
||||
paragraph sectionText
|
||||
}
|
||||
}else{
|
||||
section("No devices added") {
|
||||
paragraph "All selected devices have previously been added"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
unsubscribe()
|
||||
getChildDevices().each {
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String convertIPtoHex(ipAddress) {
|
||||
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
|
||||
return hex
|
||||
}
|
||||
|
||||
private String convertPortToHex(port) {
|
||||
String hexport = port.toString().format( '%04x', port.toInteger() )
|
||||
return hexport
|
||||
}
|
||||
@@ -27,10 +27,9 @@ definition(
|
||||
|
||||
preferences {
|
||||
page(name: "selectButton")
|
||||
page(name: "configureButton1")
|
||||
page(name: "configureButton2")
|
||||
page(name: "configureButton3")
|
||||
page(name: "configureButton4")
|
||||
for (def i=1; i<=8; i++) {
|
||||
page(name: "configureButton$i")
|
||||
}
|
||||
|
||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||
section {
|
||||
@@ -60,22 +59,45 @@ def selectButton() {
|
||||
}
|
||||
}
|
||||
|
||||
def createPage(pageNum) {
|
||||
if ((state.numButton == pageNum) || (pageNum == 8))
|
||||
state.installCondition = true
|
||||
dynamicPage(name: "configureButton$pageNum", title: "Set up button $pageNum here",
|
||||
nextPage: "configureButton${pageNum+1}", install: state.installCondition, uninstall: configured(), getButtonSections(pageNum))
|
||||
}
|
||||
|
||||
def configureButton1() {
|
||||
dynamicPage(name: "configureButton1", title: "Now let's decide how to use the first button",
|
||||
nextPage: "configureButton2", uninstall: configured(), getButtonSections(1))
|
||||
state.numButton = buttonDevice.currentState("numberOfButtons")?.longValue ?: 4
|
||||
log.debug "state variable numButton: ${state.numButton}"
|
||||
state.installCondition = false
|
||||
createPage(1)
|
||||
}
|
||||
def configureButton2() {
|
||||
dynamicPage(name: "configureButton2", title: "If you have a second button, set it up here",
|
||||
nextPage: "configureButton3", uninstall: configured(), getButtonSections(2))
|
||||
createPage(2)
|
||||
}
|
||||
|
||||
def configureButton3() {
|
||||
dynamicPage(name: "configureButton3", title: "If you have a third button, you can do even more here",
|
||||
nextPage: "configureButton4", uninstall: configured(), getButtonSections(3))
|
||||
createPage(3)
|
||||
}
|
||||
|
||||
def configureButton4() {
|
||||
dynamicPage(name: "configureButton4", title: "If you have a fourth button, you rule, and can set it up here",
|
||||
install: true, uninstall: true, getButtonSections(4))
|
||||
createPage(4)
|
||||
}
|
||||
|
||||
def configureButton5() {
|
||||
createPage(5)
|
||||
}
|
||||
|
||||
def configureButton6() {
|
||||
createPage(6)
|
||||
}
|
||||
|
||||
def configureButton7() {
|
||||
createPage(7)
|
||||
}
|
||||
|
||||
def configureButton8() {
|
||||
createPage(8)
|
||||
}
|
||||
|
||||
def getButtonSections(buttonNumber) {
|
||||
|
||||
@@ -202,7 +202,8 @@ def inputSelectionPage() {
|
||||
|
||||
section("options variations") {
|
||||
paragraph "tap these elements and look at the differences when selecting an option"
|
||||
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
|
||||
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", options: ["Thing 1", "Thing 2", "(Complicated) Thing 3"])
|
||||
input(type: "enum", name: "selectionSimpleGrouped", title: "Simple (Grouped) options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
|
||||
input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions)
|
||||
}
|
||||
|
||||
@@ -214,15 +215,15 @@ def inputSelectionPage() {
|
||||
|
||||
section("segmented") {
|
||||
paragraph "segmented should only work if there are either 2 or 3 options to choose from"
|
||||
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", groupedOptions: addGroup(["One"]))
|
||||
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", groupedOptions: addGroup(["One", "Two", "Three", "Four"]))
|
||||
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", options: ["One"])
|
||||
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", options: ["One", "Two", "Three", "Four"])
|
||||
|
||||
paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected"
|
||||
input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"])
|
||||
input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"])
|
||||
|
||||
paragraph "specifying defaultValue still works with segmented selection elements"
|
||||
input(type: "enum", name: "selectionSegmentedWithDefault", title: "defaulted to 'two'", groupedOptions: addGroup(["One", "Two", "Three"]), defaultValue: "Two")
|
||||
input(type: "enum", name: "selectionSegmentedWithDefault", style: "segmented", title: "defaulted to 'two'", options: ["One", "Two", "Three"], defaultValue: "Two")
|
||||
}
|
||||
|
||||
section("required: true") {
|
||||
@@ -231,6 +232,8 @@ def inputSelectionPage() {
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "enum", name: "selectionMultiple", title: "This allows multiple selections", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true)
|
||||
input(type: "enum", name: "selectionMultipleDefault1", title: "This allows multiple selections with a single default", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true, defaultValue: "an option")
|
||||
input(type: "enum", name: "selectionMultipleDefault2", title: "This allows multiple selections with multiple defaults", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true, defaultValue: ["an option", "another option"])
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
|
||||
@@ -72,7 +72,7 @@ def authPage() {
|
||||
log.debug "have LIFX access token"
|
||||
|
||||
def options = locationOptions() ?: []
|
||||
def count = options.size()
|
||||
def count = options.size().toString()
|
||||
|
||||
return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) {
|
||||
section("Select your location") {
|
||||
|
||||
Reference in New Issue
Block a user