Compare commits

..

23 Commits

Author SHA1 Message Date
Vinay Rao
5beae1d9fc Merge pull request #2150 from SmartThingsCommunity/staging
Rolling down staging to master
2017-07-07 16:06:50 -07:00
Vinay Rao
e5738978b0 Merge pull request #850 from workingmonk/feature/new_button_methods
DVCSMP-1741 Changes to button controller to read number of buttons from the dth
2017-07-07 14:38:39 -07:00
Jack Chi
07064eb8cc Merge pull request #2148 from natec007/MSA-2084-5
MSA-2084: Spruce Sensor DTH new model and health check
2017-07-07 12:14:09 -07:00
natec007
f519a2d828 Update spruce-sensor.groovy
Removed checkInterval attribute
sendEvent(name: "checkInterval" already in updated and installed
2017-07-06 14:27:33 -07:00
Nathan Cauffman
4da49283bf MSA-2084: Updated with new model number and health check 2017-07-06 13:22:12 -07:00
Bob Florian
7389edf795 Merge pull request #2140 from juano2310/lifx_not_null
ICP-507 - Replace location count null with 0
2017-07-06 08:29:58 -07:00
Brian Steere
23154372d2 Merge pull request #2141 from SmartThingsCommunity/every-element-update
Update Every Element with more accurate enum settings
2017-07-06 09:02:42 -05:00
juano2310
1b0437c633 ICP-507 - Replace location count null with 0
Updated with def count = options.size().toString()
2017-07-06 09:43:47 -04:00
Vinay Rao
b3d9578140 Merge pull request #2147 from dkirker/CSING-26
CSING-26 Send IAS Zone update request when refreshing for 1st party zigbee sensors
2017-07-05 22:43:38 -07:00
Donald Kirker
7549979be5 CSING-26 Send IAS Zone update request when refreshing for 1st party zigbee sensors. 2017-07-05 20:35:18 -07:00
Vinay Rao
f647be3a62 Merge pull request #2144 from SmartThingsCommunity/staging
Rolling down staging to master
2017-07-05 14:22:13 -07:00
Vinay Rao
d2f981fd34 Merge pull request #2142 from SmartThingsCommunity/production
Rolling down production to staging
2017-07-05 14:13:37 -07:00
Brian Steere
2536c69083 Update Every Element with more accurate enum settings
Added a couple of extra enum inputs to demonstrate additional states
2017-07-05 16:01:16 -05:00
Vinay Rao
a58dd2094d Merge pull request #2137 from twack/update-gopher-water-valve-initail-state
(ICP-1050) Added check gopher valve initial state in installed()
2017-07-05 13:57:44 -07:00
twack
c549a5bed0 Added check valve initial state in installed() 2017-07-04 08:10:09 -07:00
Vinay Rao
ef5fffc4bc Merge pull request #2133 from jackchi/health-dhf-16
[DHF-16] ZigBee Button Device Health Update
2017-06-30 11:17:31 -07:00
jackchi
c370e88a6b [DHF-16] ZigBee Button Device Health Update 2017-06-30 10:44:20 -07:00
Vinay Rao
ce8c50c630 Merge pull request #2128 from marstorp/icp1148EcobeeHotfix
ICP-1148 Support Thermostat Dynamic data
2017-06-27 16:29:03 -07:00
marstorp
b069669c11 ICP-1148 Support Thermostat Dynamic data
Adding default "off" thermostat mode as it is not provided by the thermostat.
2017-06-27 16:19:49 -07:00
Vinay Rao
4d61d28b42 Merge pull request #2127 from SmartThingsCommunity/master
Rolling up master to staging
2017-06-27 16:05:32 -07:00
Vinay Rao
6b1e41198c Merge pull request #2125 from SmartThingsCommunity/staging
Rolling up staging to production
2017-06-27 15:07:38 -07:00
Vinay Rao
f752a01906 Merge pull request #2108 from SmartThingsCommunity/staging
Rolling up staging to production for deploy
2017-06-20 11:29:13 -07:00
Vinay Rao
45663ffb86 changes to button controller to read number of buttons from the dth 2016-04-01 16:17:03 -07:00
13 changed files with 93 additions and 897 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

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

View File

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

View File

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

View File

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

View File

@@ -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") {
@@ -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
}

View File

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

View File

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

View File

@@ -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() {

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
}

View File

@@ -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) {

View File

@@ -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") {

View File

@@ -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") {