Skip to content

Commit

Permalink
Merge pull request #2 from diablodale/diablodale-fix-1
Browse files Browse the repository at this point in the history
added support for current and legacy sonoff-tasmota devices
  • Loading branch information
BrettSheleski authored Jan 15, 2018
2 parents fec415c + 8693999 commit c1c7fd9
Showing 1 changed file with 102 additions and 90 deletions.
192 changes: 102 additions & 90 deletions devicetypes/BrettSheleski/sonoff-tasmota.src/sonoff-tasmota.groovy
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
metadata {
definition(name: "Sonoff-Tasmota", namespace: "BrettSheleski", author: "Brett Sheleski") {
definition(name: "Sonoff-Tasmota", namespace: "BrettSheleski", author: "Brett Sheleski", ocfDeviceType: "oic.d.smartplug") {
capability "Actuator"
capability "Switch"
capability "Momentary"
capability "Polling"
capability "Refresh"
}
}

// UI tile definitions
tiles(scale: 2) {
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: "momentary.push", icon: "st.switches.switch.on", backgroundColor: "#79b821"
Expand All @@ -23,7 +24,7 @@ metadata {
details(["switch","refresh"])
}

preferences {
preferences {
input(name: "ipAddress", type: "string", title: "IP Address", description: "IP Address of Sonoff", displayDuringSetup: true, required: true)
input(name: "port", type: "number", title: "Port", description: "Port", displayDuringSetup: true, required: true, defaultValue: 80)

Expand All @@ -33,109 +34,125 @@ metadata {

section("Authentication") {
input(name: "username", type: "string", title: "Username", description: "Username", displayDuringSetup: false, required: false)
input(name: "password", type: "password", title: "Password", description: "Password", displayDuringSetup: false, required: false)
input(name: "password", type: "password", title: "Password (sent cleartext)", description: "Caution: password is sent cleartext", displayDuringSetup: false, required: false)
}
}
}

def parse(String description) {
log.debug "parse()"

def STATUS_PREFIX = "STATUS = ";
def RESULT_PREFIX = "RESULT = ";

def message = parseLanMessage(description);

if (message?.body?.startsWith(STATUS_PREFIX)) {
def statusJson = message.body.substring(STATUS_PREFIX.length())

parseStatus(statusJson);
}
else if (message?.body?.startsWith(RESULT_PREFIX)) {
def resultJson = message.body.substring(RESULT_PREFIX.length())

parseResult(resultJson);
}
simulator {
// status declarations specify messages that result from a person physically actuating the device
// this is the message that the device will send to the Device Handler’s parse(message) method
status "switch reports off": "index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:f1f55fbf-b2c8-470b-bc47-0bb4cb938f25, headers:SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkNvbnRlbnQtTGVuZ3RoOiAxNQ0KQ29ubmVjdGlvbjogY2xvc2U=, body:eyJQT1dFUiI6Ik9GRiJ9"
status "switch reports on": "index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:dc77bc98-ddb3-409b-844a-0b8864082f39, headers:SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkNvbnRlbnQtTGVuZ3RoOiAxNA0KQ29ubmVjdGlvbjogY2xvc2U=, body:eyJQT1dFUiI6Ik9OIn0="
status "poll status off": "index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:eee15c07-a24a-4fc6-8d8e-b1911ed6764f, headers:SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkNvbnRlbnQtTGVuZ3RoOiAxODkNCkNvbm5lY3Rpb246IGNsb3Nl, body:eyJTdGF0dXMiOnsiTW9kdWxlIjoxLCJGcmllbmRseU5hbWUiOiJTb25vZmYiLCJUb3BpYyI6ImRlZmF1bHQtdG9waWMiLCJCdXR0b25Ub3BpYyI6IjAiLCJQb3dlciI6MCwiUG93ZXJPblN0YXRlIjoxLCJMZWRTdGF0ZSI6MSwiU2F2ZURhdGEiOjEsIlNhdmVTdGF0ZSI6MCwiQnV0dG9uUmV0YWluIjowLCJQb3dlclJldGFpbiI6MH19"
status "poll status on": "index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:b813e07c-4984-40c8-97bd-ba7603804ad0, headers:SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkNvbnRlbnQtTGVuZ3RoOiAxODkNCkNvbm5lY3Rpb246IGNsb3Nl, body:eyJTdGF0dXMiOnsiTW9kdWxlIjoxLCJGcmllbmRseU5hbWUiOiJTb25vZmYiLCJUb3BpYyI6ImRlZmF1bHQtdG9waWMiLCJCdXR0b25Ub3BpYyI6IjAiLCJQb3dlciI6MSwiUG93ZXJPblN0YXRlIjoxLCJMZWRTdGF0ZSI6MSwiU2F2ZURhdGEiOjEsIlNhdmVTdGF0ZSI6MCwiQnV0dG9uUmV0YWluIjowLCJQb3dlclJldGFpbiI6MH19"
status "legacy switch reports off": "index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:9f55a327-be15-43fb-91ce-c04a035a3217, headers:SFRUUC8xLjEgMjAwIE9LCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbgpDb250ZW50LUxlbmd0aDogMzYKQ29ubmVjdGlvbjogY2xvc2U=, body:UkVTVUxUID0geyJQT1dFUiI6Ik9GRiJ9ClBPV0VSID0gT0ZG"
status "legacy switch reports on": "index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:9f55a327-be15-43fb-91ce-c04a035a3217, headers:SFRUUC8xLjEgMjAwIE9LCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbgpDb250ZW50LUxlbmd0aDogMzQKQ29ubmVjdGlvbjogY2xvc2U=, body:UkVTVUxUID0geyJQT1dFUiI6Ik9OIn0KUE9XRVIgPSBPTg=="
status "legacy poll status off": "index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:9f55a327-be15-43fb-91ce-c04a035a3217, headers:SFRUUC8xLjEgMjAwIE9LCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbgpDb250ZW50LUxlbmd0aDogMjE5CkNvbm5lY3Rpb246IGNsb3Nl, body:U1RBVFVTID0geyJTdGF0dXMiOnsiTW9kdWxlIjoxLCAiRnJpZW5kbHlOYW1lIjoiQmFzZW1lbnQgTGlnaHRzIiwgIlRvcGljIjoiYmFzZW1lbnQtbGlnaHRzIiwgIkJ1dHRvblRvcGljIjoiMCIsICJQb3dlciI6MCwgIlBvd2VyT25TdGF0ZSI6MywgIkxlZFN0YXRlIjoxLCAiU2F2ZURhdGEiOjEsICJTYXZlU3RhdGUiOjEsICJCdXR0b25SZXRhaW4iOjAsICJQb3dlclJldGFpbiI6MH19"
status "legacy poll status on": "index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:9f55a327-be15-43fb-91ce-c04a035a3217, headers:SFRUUC8xLjEgMjAwIE9LCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbgpDb250ZW50LUxlbmd0aDogMjE5CkNvbm5lY3Rpb246IGNsb3Nl, body:U1RBVFVTID0geyJTdGF0dXMiOnsiTW9kdWxlIjoxLCAiRnJpZW5kbHlOYW1lIjoiQmFzZW1lbnQgTGlnaHRzIiwgIlRvcGljIjoiYmFzZW1lbnQtbGlnaHRzIiwgIkJ1dHRvblRvcGljIjoiMCIsICJQb3dlciI6MSwgIlBvd2VyT25TdGF0ZSI6MywgIkxlZFN0YXRlIjoxLCAiU2F2ZURhdGEiOjEsICJTYXZlU3RhdGUiOjEsICJCdXR0b25SZXRhaW4iOjAsICJQb3dlclJldGFpbiI6MH19"

// reply declarations specify responses that the physical device will send to the Device Handler
// when it receives a certain message from the Hub
// reply "2001FF,delay 5000,2602": "command: 2603, payload: FF"
}
}

def parseStatus(String json){
log.debug "status: $json"

def status = new groovy.json.JsonSlurper().parseText(json);

def isOn = status.Status.Power == 1;

setSwitchState(isOn);
String testLegacyInput() {
String prefix = 'index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:9f55a327-be15-43fb-91ce-c04a035a3217'
//String multiBody = '''STATUS = {"Status":{"Module":1, "FriendlyName":"Basement Lights", "Topic":"basement-lights", "ButtonTopic":"0", "Power":0, "PowerOnState":3, "LedState":1, "SaveData":1, "SaveState":1, "ButtonRetain":0, "PowerRetain":0}}'''
String multiBody = '''RESULT = {"POWER":"ON"}
POWER = ON'''
def contentLength = multiBody.length()
String multiHeaders = """HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: $contentLength
Connection: close"""
String multilineString = prefix + ', headers:' + multiHeaders.bytes.encodeBase64() + ', body:' + multiBody.bytes.encodeBase64()
return multilineString
}

def parseResult(String json){
log.debug "result: $json"
def parse(String description) {
def message = parseLanMessage(description) //def message = parseLanMessage(testLegacyInput())

def result = new groovy.json.JsonSlurper().parseText(json);
// parse result from current and legacy formats
def resultJson = {}
if (message?.json) {
// current json data format
resultJson = message.json
}
else {
// legacy Content-Type: text/plain
// with json embedded in body text
def STATUS_PREFIX = "STATUS = "
def RESULT_PREFIX = "RESULT = "
if (message?.body?.startsWith(STATUS_PREFIX)) {
resultJson = new groovy.json.JsonSlurper().parseText(message.body.substring(STATUS_PREFIX.length()))
}
else if (message?.body?.startsWith(RESULT_PREFIX)) {
resultJson = new groovy.json.JsonSlurper().parseText(message.body.substring(RESULT_PREFIX.length()))
}
}

def isOn = result.POWER == "ON";

setSwitchState(isOn);
// consume and set switch state
if ((resultJson?.POWER in ["ON", 1, "1"]) || (resultJson?.Status?.Power in [1, "1"])) {
setSwitchState(true)
}
else if ((resultJson?.POWER in ["OFF", 0, "0"]) || (resultJson?.Status?.Power in [0, "0"])) {
setSwitchState(false)
}
else {
log.error "can not parse result with header: $message.header"
log.error "...and raw body: $message.body"
}
}

def setSwitchState(Boolean on){
log.debug "The switch is " + (on ? "ON" : "OFF")

sendEvent(name: "switch", value: on ? "on" : "off");
def setSwitchState(Boolean on) {
log.info "switch is " + (on ? "ON" : "OFF")
sendEvent(name: "switch", value: on ? "on" : "off")
}

def push(){
def push() {
log.debug "PUSH"
toggle(); // push is just an alias for toggle
sendCommand("Power", "Toggle")
}

def toggle(){
log.debug "TOGGLE"
sendCommand("Power", "Toggle");
// deprecated: there is no code that calls toggle()
def toggle() {
log.warn "TOGGLE is deprecated"
sendCommand("Power", "Toggle")
}

def on(){
def on() {
log.debug "ON"
sendCommand("Power", "On");
sendCommand("Power", "On")
}

def off(){
def off() {
log.debug "OFF"
sendCommand("Power", "Off");
sendCommand("Power", "Off")
}

def poll(){
def poll() {
log.debug "POLL"

requestStatus()

sendCommand("Status", null)
}

def refresh(){
def refresh() {
log.debug "REFRESH"

requestStatus();
}

def requestStatus(){
log.debug "getStatus()"

def result = sendCommand("Status", null);

return result;
sendCommand("Status", null)
}

private def sendCommand(String command, String payload){

log.debug "sendCommand(${command}:${payload})"
private def sendCommand(String command, String payload) {
log.debug "sendCommand(${command}:${payload}) to device at $ipAddress:$port"

def hosthex = convertIPtoHex(ipAddress);
def porthex = convertPortToHex(port);

device.deviceNetworkId = "$hosthex:$porthex";
if (!ipAddress || !port) {
log.warn "aborting. ip address or port of device not set"
return null;
}
def hosthex = convertIPtoHex(ipAddress)
def porthex = convertPortToHex(port)
device.deviceNetworkId = "$hosthex:$porthex"

def path = "/cm"

if (payload){
path += "?cmnd=${command}%20${payload}"
}
Expand All @@ -145,32 +162,27 @@ private def sendCommand(String command, String payload){

if (username){
path += "&user=${username}"

if (password){
path += "&password=${password}"
}
}

def result = new physicalgraph.device.HubAction(
method: "GET",
path: path,
headers: [
HOST: "${ipAddress}:${port}"
]
)

return result
def result = new physicalgraph.device.HubAction(
method: "GET",
path: path,
headers: [
HOST: "${ipAddress}:${port}"
]
)
return result
}

private String convertIPtoHex(ipAddress) {
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
log.debug "IP address entered is $ipAddress and the converted hex code is $hex"
return hex

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())
log.debug hexport
return hexport
return hexport
}

0 comments on commit c1c7fd9

Please sign in to comment.