Compare commits

..

1 Commits

Author SHA1 Message Date
Vincent Romo
0c6cc43347 MSA-2048: chime 2017-06-21 20:23:03 -07:00
8 changed files with 859 additions and 953 deletions

View File

@@ -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>
'''
}

View File

@@ -0,0 +1,813 @@
/**
* Zooz Smart Chime v1.2.1
* (Model: ZSE33)
*
* Author:
* Kevin LaFramboise (krlaframboise)
*
* URL to documentation: https://community.smartthings.com/t/release-zooz-smart-chime/77152?u=krlaframboise
*
*
* Changelog:
*
* 1.2.1 (04/23/2017)
* - SmartThings broke parse method response handling so switched to sendhubaction.
* - Bug fix for location timezone issue.
*
* 1.2 (04/14/2017)
* - Added Switch Level capability
*
* 1.1 (02/19/2017)
* - Added Health Check and self polling.
*
* 1.0 (01/16/2017)
* - Initial Release
*
* 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: "Zooz Smart Chime",
namespace: "krlaframboise",
author: "Kevin LaFramboise"
) {
capability "Actuator"
capability "Alarm"
capability "Battery"
capability "Configuration"
capability "Refresh"
capability "Switch"
capability "Tone"
capability "Health Check"
capability "Polling"
capability "Switch Level"
attribute "lastCheckin", "string"
attribute "status", "enum", ["alarm", "beep", "off", "on", "custom"]
command "customChime"
fingerprint deviceId: "0x1005", inClusters: "0x25, 0x59, 0x5A, 0x5E, 0x70, 0x71, 0x72, 0x73, 0x80, 0x85, 0x86, 0x87"
fingerprint mfr:"027A", prod:"0003", model:"0088"
}
simulator { }
preferences {
input "sirenSound", "number",
title: "Siren Sound [1-10]:",
range: "1..10",
displayDuringSetup: true,
defaultValue: sirenSoundSetting
input "sirenVolume", "number",
title: "Siren Volume [1-3]:${getNameValueSettingDesc(volumeOptions)}",
range: "1..3",
required: false,
defaultValue: sirenVolumeSetting,
displayDuringSetup: true
input "sirenLength", "number",
title: "Siren Length [0-4]:${getNameValueSettingDesc(sirenLengthOptions)}",
range: "0..4",
defaultValue: sirenLengthSetting,
required: false,
displayDuringSetup: true
input "sirenLED", "number",
title: "Siren LED [0-1]:${getNameValueSettingDesc(ledOptions)}",
range: "0..1",
defaultValue: sirenLEDSetting,
required: false,
displayDuringSetup: true
input "onChimeSound", "number",
title: "Switch On Chime Sound [1-10]:",
range: "1..10",
required: false,
displayDuringSetup: true,
defaultValue: onChimeSoundSetting
input "beepChimeSound", "number",
title: "Beep Chime Sound [1-10]:",
range: "1..10",
required: false,
displayDuringSetup: true,
defaultValue: beepChimeSoundSetting
input "chimeVolume", "number",
title: "Chime Volume [1-3]:${getNameValueSettingDesc(volumeOptions)}",
range: "1..3",
required: false,
defaultValue: chimeVolumeSetting,
displayDuringSetup: true
input "chimeRepeat", "number",
title: "Chime Repeat [1-255]:\n(1-254 = # of Cycles)\n(255 = ${noLengthMsg})",
range: "1..255",
required: false,
displayDuringSetup: true,
defaultValue: chimeRepeatSetting
input "chimeLED", "number",
title: "Chime LED [0-1]:${getNameValueSettingDesc(ledOptions)}",
range: "0..1",
defaultValue: chimeLEDSetting,
required: false,
displayDuringSetup: true
input "checkinInterval", "enum",
title: "Checkin Interval:",
defaultValue: checkinIntervalSetting,
required: false,
displayDuringSetup: true,
options: checkinIntervalOptions.collect { it.name }
input "debugOutput", "bool",
title: "Enable debug logging?",
defaultValue: true,
required: false
}
tiles(scale: 2) {
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4, canChangeIcon: true){
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
attributeState "off",
label:'Off',
action: "off",
icon: "st.Entertainment.entertainment2",
backgroundColor:"#ffffff"
attributeState "on",
label:'Chime (On)!',
action: "off",
icon:"st.Entertainment.entertainment2",
backgroundColor: "#99c2ff"
attributeState "alarm",
label:'Siren!',
action: "off",
icon:"st.alarm.alarm.alarm",
backgroundColor:"#ff9999"
attributeState "beep",
label:'Chime (Beep)!',
action: "off",
icon:"st.Entertainment.entertainment2",
backgroundColor:"#99ff99"
attributeState "custom",
label:'Chime (Custom)!',
action: "off",
icon:"st.Entertainment.entertainment2",
backgroundColor:"#cc99cc"
}
}
standardTile("playAlarm", "device.alarm", width: 2, height: 2) {
state "default",
label:'Alarm',
action:"alarm.both",
icon:"st.security.alarm.clear",
backgroundColor:"#ff9999"
state "both",
label:'Turn Off',
action:"alarm.off",
icon:"st.alarm.alarm.alarm",
background: "#ffffff"
}
standardTile("playOn", "device.switch", width: 2, height: 2) {
state "default",
label:'Turn On',
action:"switch.on",
icon:"st.Entertainment.entertainment2",
backgroundColor:"#99c2ff"
state "on",
label:'Turn Off',
action:"switch.off",
icon:"st.Entertainment.entertainment2",
background: "#ffffff"
}
standardTile("playBeep", "device.status", width: 2, height: 2) {
state "default",
label:'Beep',
action:"tone.beep",
icon:"st.Entertainment.entertainment2",
backgroundColor: "#99FF99"
state "beep",
label:'Stop',
action:"off",
icon:"st.Entertainment.entertainment2",
background: "#ffffff"
}
standardTile("turnOff", "device.off", width: 2, height: 2) {
state "default",
label:'Off',
action:"switch.off",
backgroundColor: "#ffffff"
}
standardTile("refresh", "device.refresh", width: 2, height: 2) {
state "refresh", label:'Refresh', action: "refresh", icon:"st.secondary.refresh-icon"
}
valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2){
state "battery", label:'${currentValue}% battery', unit:""
}
main "status"
details(["status", "playOn", "playBeep", "playAlarm", "turnOff", "refresh", "battery"])
}
}
def updated() {
// This method always gets called twice when preferences are saved.
if (!isDuplicateCommand(state.lastUpdated, 3000)) {
state.lastUpdated = new Date().time
state.activeEvents = []
logTrace "updated()"
initializeCheckin()
if (state.firstUpdate == false) {
def result = []
result += configure()
if (result) {
return sendResponse(result)
}
}
else {
// Skip first time updating because it calls the configure method while it's already running.
state.firstUpdate = false
}
}
}
private sendResponse(cmds) {
def actions = []
cmds?.each { cmd ->
actions << new physicalgraph.device.HubAction(cmd)
}
sendHubCommand(actions)
return []
}
private initializeCheckin() {
// Set the Health Check interval so that it pings the device if it's 1 minute past the scheduled checkin.
def checkInterval = ((checkinIntervalSettingMinutes * 60) + 60)
sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
unschedule(healthPoll)
switch (checkinIntervalSettingMinutes) {
case 5:
runEvery5Minutes(healthPoll)
break
case 10:
runEvery10Minutes(healthPoll)
break
case 15:
runEvery15Minutes(healthPoll)
break
case 30:
runEvery30Minutes(healthPoll)
break
case [60, 120]:
runEvery1Hour(healthPoll)
break
default:
runEvery3Hours(healthPoll)
}
}
def healthPoll() {
logTrace "healthPoll()"
sendHubCommand([new physicalgraph.device.HubAction(batteryGetCmd())], 100)
}
def ping() {
logTrace "ping()"
if (canCheckin()) {
logDebug "Attempting to ping device."
// Restart the polling schedule in case that's the reason why it's gone too long without checking in.
initializeCheckin()
return poll()
}
}
def poll() {
if (canCheckin()) {
logTrace "Polling Device"
return batteryGetCmd()
}
else {
logTrace "Skipped Poll"
}
}
def configure() {
logTrace "configure()"
def cmds = []
def refreshAll = (!state.isConfigured || state.pendingRefresh || !settings?.sirenSound)
if (!state.isConfigured) {
logTrace "Waiting 1 second because this is the first time being configured"
cmds << "delay 1000"
}
configData.each {
cmds += updateConfigVal(it.paramNum, it.value, refreshAll)
}
if (refreshAll) {
cmds << switchBinaryGetCmd()
cmds << batteryGetCmd()
}
if (cmds) {
logDebug "Sending configuration to device."
return delayBetween(cmds, 250)
}
else {
return cmds
}
}
private updateConfigVal(paramNum, val, refreshAll) {
def result = []
def configVal = state["configVal${paramNum}"]
if (refreshAll || (configVal != val)) {
result << configSetCmd(paramNum, val)
result << configGetCmd(paramNum)
}
return result
}
def off() {
logDebug "Turning Off()"
return sirenToggleCmds(0x00)
}
def on() {
logDebug "Playing On Chime (#${onChimeSoundSetting})"
addPendingSound("switch", "on")
return chimePlayCmds(onChimeSoundSetting)
}
def setLevel(level, rate=null) {
logTrace "Executing setLevel($level)"
if (!device.currentValue("level")) {
sendEvent(name:"level", value:0, displayed:false)
}
return customChime(extractSoundFromLevel(level))
}
private extractSoundFromLevel(level) {
def sound = safeToInt(level, 1)
if (sound <= 10) {
return 1
}
else {
if ((sound % 10) != 0) {
sound = (sound - (sound % 10))
}
return (sound / 10)
}
}
def beep() {
logDebug "Playing Beep Chime (#${beepChimeSoundSetting})"
addPendingSound("status", "beep")
return chimePlayCmds(beepChimeSoundSetting)
}
def customChime(sound) {
def val = validateSound(sound, beepChimeSoundSetting)
if ("${sound}" != "${val}") {
logDebug "Playing Custom Chime (#${val}) - (${sound} is not a valid sound number)"
}
else {
logDebug "Playing Custom Chime (#${val})"
}
addPendingSound("status", "custom")
return chimePlayCmds(val)
}
def siren() { return both() }
def strobe() { return both() }
def both() {
logDebug "Playing Siren (#${sirenSoundSetting})"
addPendingSound("alarm", "both")
if (sirenLengthSetting == 0) {
// Siren Length is set to chime.
return chimePlayCmds(sirenSoundSetting)
}
else {
def result = []
result += sirenToggleCmds(0xFF)
return result
}
}
private addPendingSound(name, value) {
state.pendingSound = [name: "$name", value: "$value", time: new Date().time]
}
def refresh() {
logTrace "refresh()"
state.pendingRefresh = true
return configure()
}
def parse(String description) {
def result = []
if (description.startsWith("Err")) {
log.warn "Parse Error: $description"
result << createEvent(descriptionText: "$device.displayName $description", isStateChange: true)
}
else {
def cmd = zwave.parse(description, getCommandClassVersions())
if (cmd) {
result += zwaveEvent(cmd)
}
else {
logDebug "Unable to parse description: $description"
}
}
if (canCheckin()) {
result << createLastCheckinEvent()
}
return result
}
private getCommandClassVersions() {
[
0x59: 1, // AssociationGrpInfo
0x5A: 1, // DeviceResetLocally
0x5E: 2, // ZwaveplusInfo
0x70: 1, // Configuration
0x71: 3, // Notification v4
0x72: 2, // ManufacturerSpecific
0x73: 1, // Powerlevel
0x80: 1, // Battery
0x85: 2, // Association
0x86: 1, // Version (2)
0x87: 1, // Indicator
0x25: 1, // Switch Binary
]
}
private canCheckin() {
def minimumCheckinInterval = ((checkinIntervalSettingMinutes * 60 * 1000) - 5000)
return (!state.lastCheckinTime || ((new Date().time - state.lastCheckinTime) >= minimumCheckinInterval))
}
private createLastCheckinEvent() {
logDebug "Device Checked In"
state.lastCheckinTime = new Date().time
return createEvent(name: "lastCheckin", value: convertToLocalTimeString(new Date()), displayed: false)
}
private convertToLocalTimeString(dt) {
def timeZoneId = location?.timeZone?.ID
if (timeZoneId) {
return dt.format("MM/dd/yyyy hh:mm:ss a", TimeZone.getTimeZone(timeZoneId))
}
else {
return "$dt"
}
}
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
logTrace "BatteryReport: $cmd"
def isNew = (device.currentValue("battery") != cmd.batteryLevel)
def val = (cmd.batteryLevel == 0xFF ? 1 : cmd.batteryLevel)
def result = []
result << createEvent(name: "battery", value: val, unit: "%", displayed: isNew, isStateChange: true)
return result
}
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
def name = configData.find { it.paramNum == cmd.parameterNumber }?.name
if (name) {
def val = cmd.configurationValue[0]
logDebug "${name} = ${val}"
state."configVal${cmd.parameterNumber}" = val
}
else {
logDebug "Parameter ${cmd.parameterNumber}: ${cmd.configurationValue}"
}
state.isConfigured = true
state.pendingRefresh = false
return []
}
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
def result = []
logTrace "BasicReport: ${cmd}"
if (cmd.value == 0x00) {
result += handleDeviceOff()
}
else {
result += sendResponse(["delay 3000", basicGetCmd()])
}
return result
}
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
def result = []
logTrace "SwitchBinaryReport: ${cmd}"
return result
}
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
def result = []
logTrace "NotificationReport: ${cmd}"
if (cmd.notificationType == 14) {
if (cmd.event == 0) {
result += handleDeviceOff()
}
else if (cmd.event == 1) {
result += handleDeviceOn(state.activeSound, state.pendingSound)
}
}
return result
}
private handleDeviceOn(activeSound, pendingSound) {
def result = []
def activeSoundName = activeSound?.name
state.activeSound = pendingSound
if (pendingSound) {
result << createEvent(getEventMap(pendingSound, true))
def statusVal = ""
if (pendingSound.name == "alarm") {
statusVal = "alarm"
}
else if (pendingSound.name == "switch") {
statusVal = "on"
}
if (statusVal) {
result << createEvent(getStatusEventMap(statusVal))
}
}
else {
logTrace "Unable to create event on because the pending sound has not been set."
}
return result
}
private handleDeviceOff() {
def result = []
["alarm", "switch", "status"].each { n ->
def displayed = false
if ("${n}" == "${state.activeSound?.name}") {
// Only the active event was initially displayed so it's the only off event that gets displayed.
displayed = true
state.activeSound = null
}
result << createEvent(getEventMap([name: "$n", value: "off"], displayed))
}
return result
}
private getStatusEventMap(val) {
return getEventMap([name: "status", value: val], false)
}
private getEventMap(event, displayed=false) {
def isStateChange = (device.currentValue(event.name) != event.value)
def eventMap = [
name: event.name,
value: event.value,
displayed: displayed,
isStateChange: true
]
logTrace "Creating Event: ${eventMap}"
return eventMap
}
def zwaveEvent(physicalgraph.zwave.Command cmd) {
logDebug "Unhandled Command: $cmd"
return []
}
private chimePlayCmds(sound) {
def cmds = []
cmds << indicatorSetCmd(sound)
if ((sound in [1, 9, 10]) && chimeRepeatSetting == 1) {
// Fixes problem where these sounds stop playing before the start events are created causing the off events to never get created.
cmds << basicGetCmd()
cmds << "delay 3000"
}
return cmds
}
private indicatorSetCmd(val) {
return zwave.indicatorV1.indicatorSet(value: val).format()
}
private sirenToggleCmds(val) {
return [
switchBinarySetCmd(val),
switchBinaryGetCmd()
]
}
private switchBinaryGetCmd() {
return zwave.switchBinaryV1.switchBinaryGet().format()
}
private switchBinarySetCmd(val) {
return zwave.switchBinaryV1.switchBinarySet(switchValue: val).format()
}
private basicGetCmd() {
return zwave.basicV1.basicGet().format()
}
private batteryGetCmd() {
return zwave.batteryV1.batteryGet().format()
}
private configGetCmd(paramNum) {
return zwave.configurationV1.configurationGet(parameterNumber: paramNum).format()
}
private configSetCmd(paramNum, val) {
return zwave.configurationV1.configurationSet(parameterNumber: paramNum, size: 1, scaledConfigurationValue: val).format()
}
// Configuration Parameters
private getConfigData() {
// [paramNum: 6, name: "Chime Sound"]
return [
[paramNum: 5, name: "Siren Sound", value: sirenSoundSetting],
[paramNum: 1, name: "Siren Volume", value: sirenVolumeSetting],
[paramNum: 2, name: "Siren Length", value: (sirenLengthSetting == 4 ? 255 : sirenLengthSetting)],
[paramNum: 8, name: "Siren LED", value: sirenLEDSetting],
[paramNum: 4, name: "Chime Volume", value: chimeVolumeSetting],
[paramNum: 3, name: "Chime Repeat", value: chimeRepeatSetting],
[paramNum: 9, name: "Chime LED", value: chimeLEDSetting],
[paramNum: 7, name: "Chime Mode", value: chimeModeSetting]
]
}
// Settings
private getDebugOutputSetting() {
return (settings?.debugOutput != false)
}
private getSirenSoundSetting() {
return validateSound(settings?.sirenSound, 9)
}
private getSirenVolumeSetting() {
return safeToInt(settings?.sirenVolume, 3)
}
private getSirenLengthSetting() {
return safeToInt(settings?.sirenLength, 0)
}
private getSirenLEDSetting() {
return safeToInt(settings?.sirenLED, 1)
}
private getOnChimeSoundSetting() {
return validateSound(settings?.onChimeSound, 1)
}
private getBeepChimeSoundSetting() {
return validateSound(settings?.beepChimeSound, 3)
}
private getChimeVolumeSetting() {
return safeToInt(settings?.chimeVolume, 3)
}
private getChimeRepeatSetting() {
return safeToInt(settings?.chimeRepeat, 1)
}
private getChimeLEDSetting() {
return safeToInt(settings?.chimeLED, 0)
}
private getChimeModeSetting() {
return 1 // Chime Mode should always be disabled.
}
private getCheckinIntervalSettingMinutes() {
return convertOptionSettingToInt(checkinIntervalOptions, checkinIntervalSetting)
}
private getCheckinIntervalSetting() {
return settings?.checkinInterval ?: findDefaultOptionName(checkinIntervalOptions)
}
private validateSound(sound, defaultVal) {
def val = safeToInt(sound, defaultVal)
if (val > 10) {
val = 10
}
else if (val < 1) {
val = 1
}
return val
}
private getVolumeOptions() {
[
[name: "Low", value: 1],
[name: "Medium", value: 2],
[name: "High", value: 3]
]
}
private getLedOptions() {
[
[name: "Off", value: 0],
[name: "On", value: 1]
]
}
private getSirenLengthOptions() {
[
[name: "Chime", value: 0],
[name: "30 Seconds", value: 1],
[name: "1 Minute", value: 2],
[name: "5 Minutes", value: 3],
[name: "${noLengthMsg}", value: 4] // config value is 255
]
}
private getNoLengthMsg() {
return "Play until battery is depleted"
}
private getNameValueSettingDesc(nameValueMap) {
def desc = ""
nameValueMap?.sort { it.value }.each {
desc = "${desc}\n(${it.value} - ${it.name})"
}
return desc
}
private getCheckinIntervalOptions() {
[
[name: "5 Minutes", value: 5],
[name: "10 Minutes", value: 10],
[name: "15 Minutes", value: 15],
[name: "30 Minutes", value: 30],
[name: "1 Hour", value: 60],
[name: "2 Hours", value: 120],
[name: "3 Hours", value: 180],
[name: "6 Hours", value: 360],
[name: "9 Hours", value: 540],
[name: formatDefaultOptionName("12 Hours"), value: 720],
[name: "18 Hours", value: 1080],
[name: "24 Hours", value: 1440]
]
}
private convertOptionSettingToInt(options, settingVal) {
return safeToInt(options?.find { "${settingVal}" == it.name }?.value, 0)
}
private formatDefaultOptionName(val) {
return "${val}${defaultOptionSuffix}"
}
private findDefaultOptionName(options) {
def option = options?.find { it.name?.contains("${defaultOptionSuffix}") }
return option?.name ?: ""
}
private getDefaultOptionSuffix() {
return " (Default)"
}
private safeToInt(val, defaultVal=-1) {
return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal
}
private isDuplicateCommand(lastExecuted, allowedMil) {
!lastExecuted ? false : (lastExecuted + allowedMil > new Date().time)
}
private logDebug(msg) {
if (debugOutputSetting) {
log.debug "$msg"
}
}
private logTrace(msg) {
// log.trace "$msg"
}

View File

@@ -70,7 +70,7 @@ metadata {
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
state "auxheatonly", action:"switchMode", icon: "st.thermostat.emergency-heat"
state "emergency heat", action:"switchMode", icon: "st.thermostat.emergency-heat" // emergency heat = auxHeatOnly
state "updating", label:"Working", icon: "st.secondary.secondary"
}
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
@@ -156,49 +156,47 @@ void poll() {
def generateEvent(Map results) {
log.debug "parsing data $results"
if(results) {
results.each { name, value ->
def linkText = getLinkText(device)
def supportedThermostatModes = []
def thermostatMode = null
results.each { name, value ->
def isChange = false
def isDisplayed = true
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
handlerName: name]
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger()
event << [value: sendValue, unit: temperatureScale]
isChange = isTemperatureStateChange(device, name, value.toString())
isDisplayed = isChange
event << [value: sendValue, unit: temperatureScale, isStateChange: isChange, displayed: isDisplayed]
} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger()
event << [value: sendValue, unit: temperatureScale, displayed: false]
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
if (value == true) {
supportedThermostatModes << ((name == "auxHeatMode") ? "auxheatonly" : name - "Mode")
}
return // as we don't want to send this event here, proceed to next name/value pair
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false]
} else if (name=="thermostatFanMode"){
sendEvent(name: "supportedThermostatFanModes", value: fanModes(), displayed: false)
event << [value: value.toString(), data:[supportedThermostatFanModes: fanModes()]]
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false]
} else if (name=="humidity") {
event << [value: value.toString(), displayed: false, unit: "%"]
isChange = isStateChange(device, name, value.toString())
event << [value: value.toString(), isStateChange: isChange, displayed: false, unit: "%"]
} else if (name == "deviceAlive") {
isChange = isStateChange(device, name, value.toString())
event['isStateChange'] = isChange
event['displayed'] = false
} else if (name == "thermostatMode") {
thermostatMode = value.toLowerCase()
return // as we don't want to send this event here, proceed to next name/value pair
def mode = value.toString()
mode = (mode == "auxHeatOnly") ? "emergency heat" : mode
isChange = isStateChange(device, name, mode)
event << [value: mode, isStateChange: isChange, displayed: isDisplayed]
} else {
event << [value: value.toString()]
isChange = isStateChange(device, name, value.toString())
isDisplayed = isChange
event << [value: value.toString(), isStateChange: isChange, displayed: isDisplayed]
}
sendEvent(event)
}
if (state.supportedThermostatModes != supportedThermostatModes) {
state.supportedThermostatModes = supportedThermostatModes
sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
}
if (thermostatMode) {
sendEvent(name: "thermostatMode", value: thermostatMode, data:[supportedThermostatModes:state.supportedThermostatModes], linkText: linkText,
descriptionText: getThermostatDescriptionText("thermostatMode", thermostatMode, linkText), handlerName: "thermostatMode")
}
generateSetpointEvent ()
generateStatusEvent ()
}
@@ -324,7 +322,15 @@ void resumeProgram() {
}
def modes() {
return state.supportedThermostatModes
if (state.modes) {
log.debug "Modes = ${state.modes}"
return state.modes
}
else {
state.modes = parent.availableModes(this)
log.debug "Modes = ${state.modes}"
return state.modes
}
}
def fanModes() {
@@ -407,13 +413,11 @@ def setThermostatFanMode(String mode) {
}
def generateModeEvent(mode) {
sendEvent(name: "thermostatMode", value: mode, data:[supportedThermostatModes: state.supportedThermostatModes],
descriptionText: "$device.displayName is in ${mode} mode")
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true)
}
def generateFanModeEvent(fanMode) {
sendEvent(name: "thermostatFanMode", value: fanMode, data:[supportedThermostatFanModes: fanModes()],
descriptionText: "$device.displayName fan is in ${fanMode} mode")
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${fanMode} mode", displayed: true)
}
def generateOperatingStateEvent(operatingState) {
@@ -449,14 +453,14 @@ def heat() {
}
def emergencyHeat() {
auxheatonly()
auxHeatOnly()
}
def auxheatonly() {
log.debug "auxheatonly()"
def auxHeatOnly() {
log.debug "auxHeatOnly = emergency heat"
def deviceId = device.deviceNetworkId.split(/\./).last()
if (parent.setMode ("auxHeatOnly", deviceId))
generateModeEvent("auxheatonly")
generateModeEvent("emergency heat") // emergency heat = auxHeatOnly
else {
log.debug "Error setting new mode."
def currentMode = device.currentState("thermostatMode")?.value
@@ -589,7 +593,7 @@ def generateSetpointEvent() {
} else if (mode == "off") {
sendEvent("name":"thermostatSetpoint", "value":averageSetpoint, "unit":location.temperatureScale)
sendEvent("name":"displayThermostatSetpoint", "value":"Off", displayed: false)
} else if (mode == "auxheatonly") {
} else if (mode == "emergency heat") { // emergency heat = auxHeatOnly
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
sendEvent("name":"displayThermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale, displayed: false)
}
@@ -628,7 +632,7 @@ void raiseSetpoint() {
targetvalue = thermostatSetpoint ? thermostatSetpoint : 0
targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5
if ((mode == "heat" || mode == "auxheatonly") && targetvalue > maxHeatingSetpoint) {
if ((mode == "heat" || mode == "emergency heat") && targetvalue > maxHeatingSetpoint) { // emergency heat = auxHeatOnly
targetvalue = maxHeatingSetpoint
} else if (mode == "cool" && targetvalue > maxCoolingSetpoint) {
targetvalue = maxCoolingSetpoint
@@ -674,7 +678,7 @@ void lowerSetpoint() {
targetvalue = thermostatSetpoint ? thermostatSetpoint : 0
targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5
if ((mode == "heat" || mode == "auxheatonly") && targetvalue < minHeatingSetpoint) {
if ((mode == "heat" || mode == "emergency heat") && targetvalue < minHeatingSetpoint) { // emergency heat = auxHeatOnly
targetvalue = minHeatingSetpoint
} else if (mode == "cool" && targetvalue < minCoolingSetpoint) {
targetvalue = minCoolingSetpoint
@@ -715,7 +719,7 @@ void alterSetpoint(temp) {
}
//step1: check thermostatMode, enforce limits before sending request to cloud
if (mode == "heat" || mode == "auxheatonly"){
if (mode == "heat" || mode == "emergency heat"){ // emergency heat = auxHeatOnly
if (temp.value > coolingSetpoint){
targetHeatingSetpoint = temp.value
targetCoolingSetpoint = temp.value
@@ -750,7 +754,7 @@ void alterSetpoint(temp) {
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
} else {
log.error "Error alterSetpoint()"
if (mode == "heat" || mode == "auxheatonly"){
if (mode == "heat" || mode == "emergency heat"){ // emergency heat = auxHeatOnly
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
sendEvent("name": "displayThermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
} else if (mode == "cool") {
@@ -779,7 +783,7 @@ def generateStatusEvent() {
log.debug "Cooling set point = ${coolingSetpoint}"
log.debug "HVAC Mode = ${mode}"
if (mode == "heat" || mode == "auxheatonly") {
if (mode == "heat") {
if (temperature >= heatingSetpoint) {
statusText = "Right Now: Idle"
} else {
@@ -802,6 +806,8 @@ def generateStatusEvent() {
}
} else if (mode == "off") {
statusText = "Right Now: Off"
} else if (mode == "emergency heat") { // emergency heat = auxHeatOnly
statusText = "Emergency Heat"
} else {
statusText = "?"
}

View File

@@ -23,6 +23,7 @@ metadata {
capability "Refresh"
capability "Sensor"
capability "Health Check"
capability "Light"
capability "Outlet"
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet"

View File

@@ -1,2 +0,0 @@
.st-ignore
README.md

View File

@@ -1,43 +0,0 @@
# ZigBee Button
Cloud Execution
Works with:
* [OSRAM LIGHTIFY Dimming Switch](https://support.smartthings.com/hc/en-us/articles/115000236823-SYLVANIA-Dimming-Switch)
* [Iris Smart Button](https://support.smartthings.com/hc/en-us/articles/115000190186-Iris-Smart-Button)
* [Iris KeyFob](https://support.smartthings.com/hc/en-us/articles/217409686-Iris-Smart-Fob)
## Table of contents
* [Capabilities](#capabilities)
* [Health](#device-health)
* [Troubleshooting](#troubleshooting)
## Capabilities
* **Actuator** - It represents that a device has commands.
* **Battery** - It defines that the device has a battery
* **Button** - It defines that a device has one or more buttons
* **Holdable Button** - It defines that a device has one or more holdable buttons
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
* **Refresh** - _refresh()_ command for status updates
* **Sensor** - it represents that a Device has attributes.
* **Health Check** - indicates ability to get device health notifications
## Device Health
SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE`
* __722min__ checkInterval
## Troubleshooting
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
Pairing needs to be tried again by placing the device closer to the hub.
It may also happen that you need to reset the device.
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
* [OSRAM LIGHTIFY Dimming Switch Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/115000236823-SYLVANIA-Dimming-Switch)
* [Iris Smart Button Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/115000190186-Iris-Smart-Button)
* [Iris KeyFob Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/217409686-Iris-Smart-Fob)

View File

@@ -24,7 +24,6 @@ metadata {
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Health Check"
command "enrollResponse"
@@ -183,13 +182,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 +190,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") {

View File

@@ -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
}