Compare commits

...

1 Commits

Author SHA1 Message Date
seunghyeon
5ca9ad368a MSA-2789: hyeon home 2018-05-07 05:25:38 -07:00
4 changed files with 1449 additions and 0 deletions

View File

@@ -0,0 +1,450 @@
/**
* 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,174 @@
/**
* Xiaomi Switch (v.0.0.1)
*
* MIT License
*
* Copyright (c) 2018 fison67@nate.com
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import groovy.json.JsonSlurper
metadata {
definition (name: "Xiaomi Button SW", namespace: "fison67", author: "fison67") {
capability "Sensor" //"on", "off"
capability "Button"
capability "Configuration"
capability "Battery"
capability "Refresh"
attribute "status", "string"
attribute "lastCheckin", "Date"
command "Lclick"
command "Rclick"
command "both_click"
command "refesh"
}
simulator {
}
tiles(scale: 2) {
multiAttributeTile(name:"button", type: "generic", width: 6, height: 4){
tileAttribute ("device.button", key: "PRIMARY_CONTROL") {
attributeState "click", label:'\nButton', icon:"http://postfiles9.naver.net/MjAxODA0MDJfOSAg/MDAxNTIyNjcwOTc2MTcx.Eq3RLdNXT6nbshuDgjG4qbfMjCob8eTjYv6fltmg7Zcg.1W8CkaPojCBp07iCYi5JYkJl5YTWxQL5aDG-TQ0XF_kg.PNG.shin4299/buttonSW_main.png?type=w3", backgroundColor:"#8CB8C9"
}
tileAttribute("device.battery", key: "SECONDARY_CONTROL") {
attributeState("default", label:'Battery: ${currentValue}%\n')
}
tileAttribute("device.lastCheckin", key: "SECONDARY_CONTROL") {
attributeState("default", label:'\nLast Update: ${currentValue}')
}
}
valueTile("btn0-click", "device.button", decoration: "flat", width: 2, height: 2) {
state "default", label:'Button#1_Core \n Left_click', action:"Lclick"
}
valueTile("btn1-click", "device.button", decoration: "flat", width: 2, height: 2) {
state "default", label:"Button#2_Core \n Right_click", action:"Rclick"
}
valueTile("both_click", "device.button", decoration: "flat", width: 2, height: 2) {
state "default", label:"Button#3_Core \n Both_click", action:"both_click"
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "default", label:"", action:"refresh", icon:"st.secondary.refresh"
}
}
}
def Lclick() {buttonEvent(1, "pushed")}
def Rclick() {buttonEvent(2, "pushed")}
def both_click() {buttonEvent(3, "pushed")}
// parse events into attributes
def parse(String description) {
log.debug "Parsing '${description}'"
}
def setInfo(String app_url, String id) {
log.debug "${app_url}, ${id}"
state.app_url = app_url
state.id = id
}
def setStatus(params){
log.debug "Mi Connector >> ${params.key} : ${params.data}"
switch(params.key){
case "action":
if(params.data == "btn0-click") {
buttonEvent(1, "pushed")
} else if(params.data == "btn1-click") {
buttonEvent(2, "pushed")
} else if(params.data == "both_click") {
buttonEvent(3, "pushed")
} else {
}
break;
case "batteryLevel":
sendEvent(name:"battery", value: params.data)
break;
}
updateLastTime()
}
def buttonEvent(Integer button, String action) {
sendEvent(name: "button", value: action, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $action", isStateChange: true)
}
def updateLastTime(){
def now = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone)
sendEvent(name: "lastCheckin", value: now)
}
def refresh(){
log.debug "Refresh"
def options = [
"method": "GET",
"path": "/devices/get/${state.id}",
"headers": [
"HOST": state.app_url,
"Content-Type": "application/json"
]
]
sendCommand(options, callback)
}
def callback(physicalgraph.device.HubResponse hubResponse){
def msg
try {
msg = parseLanMessage(hubResponse.description)
def jsonObj = new JsonSlurper().parseText(msg.body)
sendEvent(name:"battery", value: jsonObj.properties.batteryLevel)
updateLastTime()
} catch (e) {
log.error "Exception caught while parsing data: "+e;
}
}
def updated() {
}
def sendCommand(options, _callback){
def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: _callback])
sendHubCommand(myhubAction)
}
def makeCommand(body){
def options = [
"method": "POST",
"path": "/control",
"headers": [
"HOST": state.app_url,
"Content-Type": "application/json"
],
"body":body
]
return options
}

View File

@@ -0,0 +1,409 @@
/**
* 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

@@ -0,0 +1,416 @@
/**
* Mi Connector (v.0.0.1)
*
* MIT License
*
* Copyright (c) 2018 fison67@nate.com
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import groovy.transform.Field
definition(
name: "Mi Connector",
namespace: "fison67",
author: "fison67",
description: "A Connector between Xiaomi and ST",
category: "My Apps",
iconUrl: "https://github.com/fison67/mi_connector/raw/master/icon.png",
iconX2Url: "https://github.com/fison67/mi_connector/raw/master/icon.png",
iconX3Url: "https://github.com/fison67/mi_connector/raw/master/icon.png",
oauth: true
)
preferences {
page(name: "mainPage")
page(name: "monitorPage")
page(name: "langPage")
}
def mainPage() {
def languageList = ["English", "Korean"]
dynamicPage(name: "mainPage", title: "Home Assistant Manage", nextPage: null, uninstall: true, install: true) {
section("Request New Devices"){
input "address", "string", title: "Server address", required: true
input(name: "selectedLang", title:"Select a language" , type: "enum", required: true, options: languageList, defaultValue: "English", description:"Language for DTH")
href url:"http://${settings.address}", style:"embedded", required:false, title:"Management", description:"This makes you easy to setup"
}
section() {
paragraph "View this SmartApp's configuration to use it in other places."
href url:"${apiServerUrl("/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}")}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
}
}
}
def langPage(){
dynamicPage(name: "langPage", title:"Select a Language") {
section ("Select") {
input "Korean", title: "Korean", multiple: false, required: false
}
}
}
def installed() {
log.debug "Installed with settings: ${settings}"
if (!state.accessToken) {
createAccessToken()
}
initialize()
}
def updated() {
log.debug "Updated with settings: ${settings}"
// Unsubscribe from all events
unsubscribe()
// Subscribe to stuff
initialize()
}
def updateLanguage(){
log.debug "Languge >> ${settings.selectedLang}"
def list = getChildDevices()
list.each { child ->
try{
child.setLanguage(settings.selectedLang)
}catch(e){
log.error "DTH is not supported to select language"
}
}
}
def initialize() {
log.debug "initialize"
def options = [
"method": "POST",
"path": "/settings/smartthings",
"headers": [
"HOST": settings.address,
"Content-Type": "application/json"
],
"body":[
"app_url":"${apiServerUrl}/api/smartapps/installations/",
"app_id":app.id,
"access_token":state.accessToken
]
]
log.debug options
def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: null])
sendHubCommand(myhubAction)
updateLanguage()
}
def dataCallback(physicalgraph.device.HubResponse hubResponse) {
def msg, json, status
try {
msg = parseLanMessage(hubResponse.description)
status = msg.status
json = msg.json
log.debug "${json}"
state.latestHttpResponse = status
} catch (e) {
logger('warn', "Exception caught while parsing data: "+e);
}
}
def getDataList(){
def options = [
"method": "GET",
"path": "/requestDevice",
"headers": [
"HOST": settings.address,
"Content-Type": "application/json"
]
]
def myhubAction = new physicalgraph.device.HubAction(options, null, [callback: dataCallback])
sendHubCommand(myhubAction)
}
def addDevice(){
def id = params.id
def type = params.type
def data = params.data
if(data){
data = new JsonSlurper().parseText(data)
}
log.debug("Try >> ADD Xiaomi Device id=${id}, type=${type}")
log.debug("Data >> ${data}")
def dni = "mi-connector-" + id.toLowerCase()
def chlid = getChildDevice(dni)
if(!child){
def dth = null
def name = null
if(params.type == "zhimi.airpurifier.m1" || params.type == "zhimi.airpurifier.v1" || params.type == "zhimi.airpurifier.v2" || params.type == "zhimi.airpurifier.v3" || params.type == "zhimi.airpurifier.v6" || params.type == "zhimi.airpurifier.ma2"){
dth = "Xiaomi Air Purifier";
name = "Xiaomi Air Purifier";
}else if(params.type == "lumi.gateway.v2"){
dth = "Xiaomi Gateway";
name = "Xiaomi Gateway V2";
}else if(params.type == "lumi.gateway.v3"){
dth = "Xiaomi Gateway";
name = "Xiaomi Gateway V3";
}else if(params.type == "lumi.magnet" || params.type == "lumi.magnet.aq2"){
dth = "Xiaomi Door";
name = "Xiaomi Door";
}else if(params.type == "lumi.motion" || params.type == "lumi.motion.aq2"){
dth = "Xiaomi Motion";
name = "Xiaomi Motion";
}else if(params.type == "lumi.switch"){
dth = "Xiaomi Button Ori";
name = "Xiaomi Button Ori";
}else if(params.type == "lumi.switch.v2"){
dth = "Xiaomi Button AQ";
name = "Xiaomi Button AQ";
}else if(params.type == "lumi.86sw1"){
dth = "Xiaomi Button SW1";
name = "Xiaomi Button SW1";
}else if(params.type == "lumi.86sw2"){
dth = "Xiaomi Button SW";
name = "Xiaomi Button SW";
}else if(params.type == "lumi.cube"){
dth = "Xiaomi Cube";
name = "Xiaomi Cube";
}else if(params.type == "zhimi.humidifier.v1" || params.type == "zhimi.humidifier.ca1"){
dth = "Xiaomi Humidifier";
name = "Xiaomi Humidifier";
}else if(params.type == "zhimi.fan.v3"){
dth = "Xiaomi Fan";
name = "Xiaomi Fan";
}else if(params.type == "zhimi.airmonitor.v1"){
dth = "Xiaomi Air Monitor";
name = "Xiaomi Air Monitor";
}else if(params.type == "yeelink.light.color1"){
dth = "Xiaomi Light";
name = "Xiaomi Light";
}else if(params.type == "yeelink.light.strip1"){
dth = "Xiaomi Light Strip";
name = "Xiaomi Light Strip";
}else if(params.type == "yeelink.light.lamp1" || params.type == "yeelink.light.mono1"){
dth = "Xiaomi Light Mono";
name = "Xiaomi Light Mono";
}else if(params.type == "philips.light.sread1" || params.type == "philips.light.bulb"){
dth = "Xiaomi Light";
name = "Philips Light";
}else if(params.type == "rockrobo.vacuum.v1" || params.type == "roborock.vacuum.s5"){
dth = "Xiaomi Vacuums";
name = "Xiaomi Vacuums";
}else if(params.type == "qmi.powerstrip.v1" || params.type == "zimi.powerstrip.v2"){
dth = "Xiaomi Power Strip";
name = "Xiaomi Power Strip";
}else if(params.type == "chuangmi.plug.v1" || params.type == "chuangmi.plug.v2" || params.type == "chuangmi.plug.m1" || params.type == "lumi.plug"){
dth = "Xiaomi Power Plug";
name = "Xiaomi Power Plug";
}else if(params.type == "lumi.ctrl_neutral1" || params.type == "lumi.ctrl_ln1" ){
dth = "Xiaomi Wall Switch";
name = "Xiaomi Wall Switch";
}else if(params.type == "lumi.ctrl_neutral2" || params.type == "lumi.ctrl_ln2"){
dth = "Xiaomi Wall Switch";
name = "Xiaomi Wall Switch";
}else if(params.type == "lumi.sensor_ht"){
dth = "Xiaomi Sensor HT";
name = "Xiaomi Sensor HT";
}else if(params.type == "zhimi.airmonitor.v1"){
dth = "Xiaomi Air Monitor";
name = "Xiaomi Air Monitor";
}else if(params.type == "lumi.weather"){
dth = "Xiaomi Weather";
name = "Xiaomi Weather";
}else if(params.type == "lumi.smoke"){
dth = "Xiaomi Smoke Detector";
name = "Xiaomi Smoke Dectector";
}else if(params.type == "lumi.gas"){
dth = "Xiaomi Gas Detector";
name = "Xiaomi Gas Dectector";
}else if(params.type == "lumi.water"){
dth = "Xiaomi Water Detector";
name = "Xiaomi Water Dectector";
}
if(dth == null){
log.warn("Failed >> Non exist DTH!!! Type >> " + type);
def resultString = new groovy.json.JsonOutput().toJson("result":"nonExist")
render contentType: "application/javascript", data: resultString
}else if(params.type == "lumi.ctrl_neutral1" || params.type == "lumi.ctrl_ln1"){
try{
def childDevice = addChildDevice("fison67", dth, (dni + "-1"), location.hubs[0].id, [
"label": name + "1"
])
childDevice.setInfo(settings.address, id, "1")
try{ childDevice.refresh() }catch(e){}
try{ childDevice.setLanguage(settings.selectedLang) }catch(e){}
log.debug "Success >> ADD Device : ${type} DNI=${dni}"
def resultString = new groovy.json.JsonOutput().toJson("result":"ok")
render contentType: "application/javascript", data: resultString
}catch(e){
log.error "Failed >> ADD Device Error : ${e}"
def resultString = new groovy.json.JsonOutput().toJson("result":"fail")
render contentType: "application/javascript", data: resultString
}
}else if(params.type == "lumi.ctrl_neutral2" || params.type == "lumi.ctrl_ln2"){
try{
def index = 1;
for (def i = 0; i <2; i++) {
def childDevice = addChildDevice("fison67", dth, (dni + "-" + index), location.hubs[0].id, [
"label": name + index
])
childDevice.setInfo(settings.address, id, index.toString())
try{ childDevice.refresh() }catch(e){}
try{ childDevice.setLanguage(settings.selectedLang) }catch(e){}
log.debug "Success >> ADD Device : ${type} DNI=${dni}"
index += 1
}
def resultString = new groovy.json.JsonOutput().toJson("result":"ok")
render contentType: "application/javascript", data: resultString
}catch(e){
log.error "Failed >> ADD Device Error : ${e}"
def resultString = new groovy.json.JsonOutput().toJson("result":"fail")
render contentType: "application/javascript", data: resultString
}
}else{
try{
def childDevice = addChildDevice("fison67", dth, dni, location.hubs[0].id, [
"label": name
])
childDevice.setInfo(settings.address, id)
log.debug "Success >> ADD Device : ${type} DNI=${dni}"
try{ childDevice.refresh() }catch(e){}
try{ childDevice.setLanguage(settings.selectedLang) }catch(e){}
def resultString = new groovy.json.JsonOutput().toJson("result":"ok")
render contentType: "application/javascript", data: resultString
}catch(e){
console.log("Failed >> ADD Device Error : " + e);
def resultString = new groovy.json.JsonOutput().toJson("result":"fail")
render contentType: "application/javascript", data: resultString
}
}
}
}
def updateDevice(){
// log.debug "Mi >> ${params.type} (${params.key}) >> ${params.cmd}"
def id = params.id
def dni = "mi-connector-" + id.toLowerCase()
def chlid = getChildDevice(dni)
if(chlid){
chlid.setStatus(params)
}
def resultString = new groovy.json.JsonOutput().toJson("result":true)
render contentType: "application/javascript", data: resultString
}
def deleteDevice(){
def id = params.id
def dni = "mi-connector-" + id.toLowerCase()
log.debug "Try >> DELETE child device(${dni})"
def result = false
def chlid = getChildDevice(dni)
if(!child){
try{
deleteChildDevice(dni)
result = true
log.debug "Success >> DELETE child device(${dni})"
}catch(err){
log.error("Failed >> DELETE child Device Error!!! ${dni} => " + err);
}
}
def resultString = new groovy.json.JsonOutput().toJson("result":result)
render contentType: "application/javascript", data: resultString
}
def getDeviceList(){
def list = getChildDevices();
def resultList = [];
list.each { child ->
// log.debug "child device id $child.deviceNetworkId with label $child.label"
def dni = child.deviceNetworkId
resultList.push( dni.substring(13, dni.length()) );
}
def configString = new groovy.json.JsonOutput().toJson("list":resultList)
render contentType: "application/javascript", data: configString
}
def authError() {
[error: "Permission denied"]
}
def renderConfig() {
def configJson = new groovy.json.JsonOutput().toJson([
description: "Mi Connector API",
platforms: [
[
platform: "SmartThings Mi Connector",
name: "Mi Connector",
app_url: apiServerUrl("/api/smartapps/installations/"),
app_id: app.id,
access_token: state.accessToken
]
],
])
def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
render contentType: "text/plain", data: configString
}
mappings {
if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) {
path("/config") { action: [GET: "authError"] }
path("/list") { action: [GET: "authError"] }
path("/update") { action: [POST: "authError"] }
path("/add") { action: [POST: "authError"] }
path("/delete") { action: [POST: "authError"] }
} else {
path("/config") { action: [GET: "renderConfig"] }
path("/list") { action: [GET: "getDeviceList"] }
path("/update") { action: [POST: "updateDevice"] }
path("/add") { action: [POST: "addDevice"] }
path("/delete") { action: [POST: "deleteDevice"] }
}
}