How can I receive LAN messages on the ST hub (when the messages can come at any point)?

smartapp

(Matthew A.) #1

I’m trying unsuccessfully to figure out how to receive LAN HTTP requests (or any kind of requests, really) on my ST hub. The requests can come at any time, so a simple response to an HTTP request made from the hub won’t work.

I’m running a server on a Raspberry Pi, and there are a few things I’d like to do with it. For one, I’d like the Pi to ping my PC periodically; if it receives a response that the PC is unreachable, I’d like it to message the ST hub, which would then shut off a virtual switch.

(The virtual switch is used to actually turn on the PC; my Amazon Echo is tied to the switch, so that when I say, “Alexa, turn on my PC,” the virtual switch is toggled, the hub sends a GET request to the Pi, and the Pi sends WOL packets to my PC. The problem is that the switch doesn’t turn off when my PC shuts down, so saying “Alexa, turn on my PC” after the PC shuts down doesn’t cause a change in the switch’s state, so the GET request to the Pi isn’t sent.)

I’d also like to be able to update the hub with ping responses sent to my phone from the Pi. For whatever reason, the hub is unreliable at detecting presence via my phone, and the Pi would do a much better job, which would open up some possibilities. (The Pi also doesn’t outsource its scheduling to the cloud, so I think I could squeeze out more reliable time-based events if I used it instead of runIn().)


(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #2

Start with the documentation and then find examples like Hue Connect and several others?

http://docs.smartthings.com/en/latest/cloud-and-lan-connected-device-types-developers-guide/building-lan-connected-device-types/index.html


(Matthew A.) #3

I examined Building the Device Type before posting, and nothing inside covers what I’m asking. I don’t know of any examples that do what I’m trying to do, either; I basically want to set up a local server on the ST hub, because I’m not looking for immediate responses to requests that the hub sends. The requests can come at any point.


(ActionTiles.com co-founder Terry @ActionTiles; GitHub: @cosmicpuppy) #4

I think the Hub is currently unable to do this locally.

Instead, you can create a Web Services SmartApp that responds to REST-API calls over HTTPS though. They go to the SmartThings Cloud where your SmartApp executes but has access to all your Devices via the Hub.


(Dan) #5

Check out this example… I tried this code a few months back using an Arduino with Ethernet Shield and it worked great. Should be simple to modify it to work with a Raspberry Pi. Hopefully, none of the ST upgrades have broken this…

https://community.smartthings.com/t/how-to-send-data-from-my-lan-connected-device-to-hubv2/28825/13?u=ogiewon&source_topic_id=54912

( I hate Mondays) #6

@tgauchat is right. A smartapp/dth can receive and parse local lan messages only on their own request. You cannot initiate local messages to the hub just yet. You can use REST cloud requests to a smartapp though. Also, ST supports WOL now


Squeezebox and Smarthings
(Ben W) #7

You can do this from the pi. If a certain event happens on the PI call a rest endpoint in ST.

Here is the docs to get started with that: http://docs.smartthings.com/en/latest/smartapp-web-services-developers-guide/tutorial-part2.html?highlight=api


( I hate Mondays) #8

Correct but they would not be LAN messages anymore, they would go out to the cloud. Only option at the moment.


(Eric M) #9

This is something that is possible, but I don’t believe it is documented (that I know of). I use it quite a bit in my esp8266 projects and other HTTP LAN projects. There are just a few guidelines that you need to follow:

  1. You need a device created in SmartThings to represent the Raspberry Pi. The device should have a Device Network Id (DNI) that matches the MAC address of the Raspberry Pi. It should be all uppercase and without any octet separators (000A959D6816).

  2. You need to send the data from the Raspberry Pi to the SmartThings Hub’s local ip and local server port. I haven’t seen a hub that doesn’t use port 39500 as its local server port, but you can use the following commands in your device handler to find the information dynamically: device.hub.getDataValue(“localIP”); device.hub.getDataValue(“localSrvPortTCP”).

  3. The Raspberry Pi should send its data using HTTP. The easiest method from Linux would be to use the curl binary.

Here is a simple example of a device handler that can parse messages that are sent to it in json format. You don’t have to use json, but I find it easy to parse and widely used.

import groovy.json.JsonSlurper

metadata {
	definition (name: "Simple LAN Example", namespace: "erocm123", author: "Eric Maycock") {
	capability "Sensor"
        
	}
   
	tiles (scale: 2){      
        valueTile("hubInfo", "device.hubInfo", decoration: "flat", height: 2, width: 6, inactiveLabel: false, canChangeIcon: false) {
            state "hubInfo", label:'${currentValue}'
        }
    }
	main("hubInfo")
	details(["hubInfo"])
}


def parse(description) {
    def events = []
    def descMap = parseDescriptionAsMap(description)
    def body = new String(descMap["body"].decodeBase64())
    def slurper = new JsonSlurper()
    def result = slurper.parseText(body)
    log.debug result

    if (result.containsKey("message")) {
       events << createEvent(name:"hubInfo", value:result.message)
    }
    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()):""]
	}
}

Here is an example of this in action. From the Raspberry Pi:

root@raspberrypi:~# curl -H “Content-Type: application/json” -X POST -d ‘{“message”:“This is a test”}’ http://x.x.x.x:39500
root@raspberrypi:~# curl -H “Content-Type: application/json” -X POST -d ‘{“message”:“Hi, this is Raspberry Pi”}’ http://x.x.x.x:39500
root@raspberrypi:~# curl -H “Content-Type: application/json” -X POST -d ‘{“message”:“Ping is down on PC1!”}’ http://x.x.x.x:39500

With this framework you can have the device handler create any kind of event that you would like and have any other device in the SmartThings ecosystem respond accordingly. For some more complex examples of the above technique you can reference this.


(Matthew A.) #10

Thanks a lot for the unbelievably thorough response! I’ll be implementing this over the weekend, but looking your example over, I’m pretty confident this should do exactly what I’m hoping for. Thank you!


(Matthew A.) #11

Just wanted to check back in and verify that this absolutely works. I can now pass LAN HTTP messages between the Pi and the ST hub, which has opened up all sorts of possibilities. Thanks again, Eric!


(Eric M) #12

No problem, glad it was able to help you. Now if only it could be put into the official documentation, or a FAQ somewhere. :wink:


(JBrown) #13

Can you give a example how you got it to work? Thanks


(Eric M) #14

@Jason_Brown it looks like you may have already seen this, but a few posts up there are some examples.


(Matthew A.) #15

No problem. I’m using it for a couple different things at the moment. I’ll outline how I’m using it, and just reply to the thread if you’d like implementation specifics for either.

Presence detection
SmartThings presence detection (through the app, which I think just detects whether or not your phone is connected to your home network) is finicky. It seems to be reliable with “inbound” presence (detecting when you’ve arrived once your phone connects to the network), but it’s been extremely unreliable with “outbound” presence (detecting when you’ve left once your phone disconnects from the network). Using this for something like “Away” mode meant that “Away” mode was frequently never triggered.

I’ve been implementing a setup over the past couple months that has so far been 100% reliable at detecting when I’m home, using the Pi (and a server I wrote in Go, as well as a Bash script), erocm1231’s Raspberry Pi device handler, and a couple sensors (a motion sensor in the entry hallway of my apartment and an open/close sensor on my front door). Note that I live alone in an apartment; this would need modification for different living arrangements.

In a nutshell:

  • A Bash script running on the Pi pings my phone every few minutes. (I had to dive into the phone’s settings to turn off an option that puts the WiFi to sleep when the device is in power-saving mode to get this to work.) If the ping is successful, I send a GET request to the Pi device handler, which updates a boolean (stored as a state variable in the Groovy code I’m running for my ST routines) that says “my phone is home.” If unsuccessful, the boolean is set to false; I’m not home.

  • When the open/close sensor reports “active,” I poll the motion sensor (using the ST Groovy) in the hallway to check for motion. If it reports motion, this means that someone is leaving the apartment. Otherwise, someone’s entering.

  • If someone’s leaving, I schedule a routine using RunIn (in the ST Groovy) that polls all the motion sensors in my apartment three times (at intervals) for motion. If no motion is reported during any of these polls, and if the boolean for my phone is set to “false” (i.e., I’m not home), then I enter “Away” mode. Again, this has worked 100% of the time over the past couple months, and I’m entering/leaving at least a couple times a day.

  • If someone’s arriving, I schedule a boolean check in 30 seconds to make sure my phone is present. If it is, this means I’m home, so I set the mode to whatever it was before I left (or whatever it should be, depending on the time of day). On the other hand, if the boolean check fails, I then enter “Alarm” mode, because it means that someone’s in my apartment and my phone isn’t present. In this mode, I regularly poll motion in the living room. If any is detected, I send a GET request to the Pi to start recording a video stream (using VLC, RTSP, and a URL I sniffed for a cheap WiFi camera I have that tells the camera to start recording), which uploads at one-minute intervals to every device I own (using Resilio Sync), and also sends me a text on my phone. (This has actually come in handy; during a maintenance call for replacing a washing machine, the technician they sent spent several minutes snooping around my apartment. I got a text at school, and the video was available on my laptop within a minute or two.)

Device State
This was my original goal with the thread I posted; I wanted ST to know whether or not my PC was on or off. I’m controlling the PC using a virtual switch, which is accessible to my Amazon Echo, so I can say, “Alexa, turn on my PC,” and she sends a GET request to the Pi (which it receives through the Go server), which then uses WakeOnLAN to send magic packets to turn on my PC. The problem was that ST lost track of the device state; if I manually turned off the PC (instead of using Alexa or ST), the virtual switch remained on, and further requests to turn it on were ignored, as there was no change in state.

Overcoming this was pretty simple: just ping the PC periodically from the Pi. If it’s on, send a GET request to erocm1231’s device handler which turns the switch on, and if it’s off, do the opposite. This lets ST keep track of the device, so any time I ask Alexa to turn it on/off, the requests work as they should.

There’s more you could do with 2-way communication with an always-on Pi, but I’m in my final semester at school, and haven’t had much time.


(Michael Tan) #16

Anyone got this working with a aruino?


(Dan) #17

Maybe something like this?


(Michael Tan) #18

Thanks ill try it! Sorry for the mulitple questions about this


(Peter M H) #19

Eric, I appreciate all of your work with SONOFF (esp8266).

I have a SonoffTH running your BIN and I modified another to use a SR04 sonic range finder, running espeasy R120

I have been trying for that couple of weeks to get the JSON results into ST device, to display the distance measurement. The http://192.168.x.xxx.)/json gets this.

I have made several versions of the device handler many sampling yours and other code.

import groovy.json.JsonSlurper

preferences {
input("ip", "text", title: "IP Address", description: "ip", required: true)
input("port", "text", title: "Port", description: "port", required: true)
input("mac", "text", title: "MAC Addr", description: "mac")
}

metadata {
definition (name: "ESP8266", namespace: "phealt", author: "Peter Healt") {
	capability "Refresh"
    capability "Sensor"
    capability "Polling"
    attribute "Distance", "string"
}

//  tile definitions
tiles {
	valueTile("distance", "device.distance", width: 2, height: 2) {
		state("distance", label:'${currentValue}', unit:"cm",
			backgroundColors:[
				[value: 31, color: "#153591"],
				[value: 44, color: "#1e9cbb"],
				[value: 59, color: "#90d2a7"],
				[value: 74, color: "#44b621"],
				[value: 84, color: "#f1d801"],
				[value: 95, color: "#d04e00"],
				[value: 96, color: "#bc2323"]
			]
		)
	}
	standardTile("refresh", "device.distance", inactiveLabel: false, decoration: "flat") {
		state "default", label:'6', action:"refresh.refresh", icon:"st.secondary.refresh"
	}
    main "distance"
	details("distance","refresh")
}
}

def installed() {
log.debug "installed()"
configure()
}

def updated() {
log.debug "updated()"


}



def parse(description) {
def events = []
def descMap = parseDescriptionAsMap(description)
def body = new String(descMap["body"].decodeBase64())
def slurper = new JsonSlurper()
def result = slurper.parseText(body)
log.debug result

if (result.containsKey("distance")) {
   events << createEvent(name:"distance", value:result.message)
}
return events
}



def configureInstant(ip, port, pos){
return getAction("/config?haip=${ip}&haport=${port}&pos=${pos}")
}



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()):""]
}
log.debug "Map"
}


def refresh() {
log.debug "refreshing"
def cmds = []
cmds << getAction("/json")
return cmds
}

private getAction(uri){ 
updateDNI()

def userpass

 if(password != null && password != "") 
userpass = encodeCredentials("admin", password)

 def headers = getHeader(userpass)

 def hubAction = new physicalgraph.device.HubAction(
method: "GET",
path: "/json",
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 = "/cmd?=reboot"
getAction(uri)
}

def sync(ip, port) {
def existingIp = getDataValue("ip")
def existingPort = getDataValue("port")
if (ip && ip != existingIp) {
    updateDataValue("ip", ip)
}
if (port && port != existingPort) {
    updateDataValue("port", port)
}
}

When I press the refresh in the ST app on the device I get this in the log…


but I can’t even get the value or even the JSON body in the log, any help would be great.

Thanks


(cjcharles) #20

I similarly had lots of problems getting smart things to receive the Json and then i discovered the two areas i was failing in:
1 on the actual ESP itself, i was passing strings between functions and they were being expired by the time they were sent, hence garbage Json was sent to smartthings and either making nothing happen or giving errors. Also i found i was not sending to the right IP address as it was not being interrupted as a string it was as signed chars.
2 on the smartthings setup, i had the Mac address added but either debug logs werent printing everything (known bug that if you put too much to debug logs then it shows nothing) or it was too do with the fact that smartthings processes the Json in an asynchronous way, hence fields were in totally the wrong order.

It took me ages to debug, but if you have a look at my code in the thread below it does give a few different examples of how I’ve done it. My code is far from good (i haven’t coded in 10 years) but it is fairly well commented.