SmartThings Smart Home Dashboard #2

Now that we’ve completed SHD #1 and got a public URL that will allow SmartThings to talk to our server, it’s time to create our SmartApp. We’re going to start with the basics to make sure we have everything working before we start adding too much complexity. This SmartApp will simply subscribe to the events for a contact sensor (open/closed) and will send commands to a switch based on those events. So when we get an event telling us the contact sensor was closed, we will turn off the switch, and when we get an open event, we will turn on the switch.

Getting Setup

First things first. Let’s make sure that the environment is setup for us to work. Open a terminal window and run the following commands. (These may already be done, but it won’t hurt anything to run again.)

sudo apt-get update && sudo apt-get upgrade
sudo pip3 install flask
sudo pip3 install flask-socketio
sudo pip3 install eventlet
sudo pip3 install requests

Creating the Basic Webhook SmartApp…

  1. Let’s start with the foundation. On your Pi, create a new folder named /home/pi/st_webhook and save the following Python code into it as st_webhook.py.
#!/usr/bin/env python

#eventlet WSGI server
import eventlet
eventlet.monkey_patch()

#Flask Libs
from flask import Flask, abort, request, jsonify

#Web Sockets
from flask_socketio import SocketIO, send, emit, join_room, leave_room, disconnect, rooms

#HTTP Libs
import requests

#JSON Libs
import json

ST_WEBHOOK = '' # This will be filled in once we have an appId

app = Flask(__name__)
socketio = SocketIO(app)

@app.route('/', methods=['POST', 'GET'])
def smarthings_requests():
	if request.method == 'GET':
		return 'GET: OK'
	
	return 'POST: OK'

if __name__ == '__main__':
	socketio.run(app, debug=True, host='0.0.0.0', port=5000)

Notice were using SocketIO to run our server even though we aren’t using web sockets (yet). This is because SocketIO will run the production WSGI server (eventlet) for us. Run the server to make sure we have a functional base:

cd /home/pi/st_webhook
python3 st_webhook.py

Go ahead and test from a browser just to make sure it’s returning the expected GET: OK response. You can just test using the browser on the Pi and entering the address: localhost:5000/ to make sure the server is running and responding. No need to test the POST method as long as the GET is working.

  1. This might be a good time to open the SmartThings Developer Documentation so you can look through it as we go. It’s also possible that, depending on when you see this, that some of the information I’m giving you here has been updated. So you’ll know you’ve got the latest info if you refer to the docs. SmartThings will only call the index (‘/’) route using the POST method, so that’s were we need to focus our code. They pass a json payload with a ‘lifecycle’ key to indicate the purpose of the call. The remainder of the json payload will flex to fit the specific lifecycle being executed. Here’s a link to the official documentation regarding Lifecycles. The Lifecycles that we will need to code for are:
    • PING: Although I’ve never seen this used, it says it’s required in the documentation. So just to be safe…
    • CONFIRMATION: This is used to confirm with SmartThings that our server is publicly accessible and that we can call back to SmartThings with the confirmation URL that they passed us. This lifecycle will be called each time you update your SmartThings SmartApp configuration to point to the new ngrok URL created each time you start your tunnel (unless you are using a paid plan).
    • CONFIGURATION: This is used by SmartThings to get our SmartApp configuration page and our requested permissions to various devices. It has two PHASES:
      • INITIALIZE: Asks our SmartApp for information such as the name and description of our SmartApp, any broad device permissions that we need, and asks us to point it to the first page of configurations settings.
      • PAGE: Asks our SmartApp for our page-level configuration. This is what will be shown in the SmartThings App when the user either Installs or Updates our SmartApp.
    • INSTALL: This is the Lifecycle SmartThings uses to let us know that the user has completed filling out our Configuration page and has agreed to our requested permissions. This is our first opportunity to subscribe to any events that we are interested in, such as switches being turned on or off.
    • UPDATE: This is used when the user goes back into our SmartApp in the SmartThings app and changes the configuration, such as adding or removing a selected device. This is our other opportunity to update our subscriptions so we can monitor events that occur with the new devices.
    • EVENT: This is used to let our SmartApp know about any events that we have subscribed to.
    • OAUTH_CALLBACK: This is used by third-parties to respond to oAuth requests by our SmartApp. We won’t be doing any of that, but we’ll put the placeholder in so it’s there if we need it in the future.
    • UNINSTALL: This is used to let us know that our SmartApp has been uninstalled.
  1. Let’s comment out the return ‘POST: OK’ and get the incoming json payload to determine the lifecycle:
@app.route('/', methods=['POST', 'GET'])
def smarthings_requests():
	if request.method == 'GET':
		return 'GET: OK'

	#return 'POST: OK'
	
	content = request.get_json()
	print('AppId: %s\nLifeCycle: %s' % (content['appId'], content['lifecycle']))

Notice here that we are printing out the AppId as well as the Lifecycle. Each SmartApp will be given an appId once it is deployed. That appId won’t change and will be synonymous with your SmartApp. Once we get the appId assigned during the SmartApp configuration in the SmartThings Developer Workspace, we’ll add it to our ST_WEBHOOK constant. Each Lifecycle call will also have an installedAppId which will represent each instance of your app that has been installed through the SmartThings App.

  1. Now we’ll add the various lifecycles:
@app.route('/', methods=['POST', 'GET'])
def smarthings_requests():
	if request.method == 'GET':
		return 'GET: OK'

	#return 'POST: OK'
	
	content = request.get_json()
	print('AppId: %s\nLifeCycle: %s' % (content['appId'], content['lifecycle']))

	if (content['lifecycle'] == 'PING'):
		print('PING: %s' % content)
		challenge = content['pingData']['challenge']
		data = {'pingData':{'challenge': challenge}}
		return jsonify(data)
		
	elif (content['lifecycle'] == 'CONFIRMATION'):
		confirmationURL = content['confirmationData']['confirmationUrl']
		r = requests.get(confirmationURL)
		print('CONFIRMATION\nContent: %s\nURL: %s\nStatus: %s' % (content,confirmationURL,r.status_code))
		if r.status_code == 200:
			return r.text
		else:
			abort(r.status_code)

	elif (content['lifecycle'] == 'CONFIGURATION' and content['configurationData']['phase'] == 'INITIALIZE'):
		print(content['configurationData']['phase'])
				
		if content['appId'] == ST_WEBHOOK:
			data = {
					  "configurationData": {
						"initialize": {
						  "name": "ST Test Webhook App",
						  "description": "ST Test Webhook App",
						  "id": "st_webhook_app_page_1",
						  "permissions": [
						  ],
						  "firstPageId": "1"
						}
					  }
					}
		else:
			data = {'appId':'Not Recognized'}
			print('Initialize Unknown appId: %s' % content['appId'])

		return jsonify(data)

	elif (content['lifecycle'] == 'CONFIGURATION' and content['configurationData']['phase'] == 'PAGE'):
		print(content['configurationData']['phase'])
		pageId = content['configurationData']['pageId']
		
		if content['appId'] == ST_WEBHOOK:
			data = {
					  "configurationData": {
						"page": {
						  "pageId": "1",
						  "name": "Select Devices",
						  "nextPageId": "null",
						  "previousPageId": "null",
						  "complete": "true",
						  "sections": [
							{
							  "name": "When this opens/closes...",
							  "settings": [
								{
								  "id": "contactSensor",
								  "name": "Which contact sensor?",
								  "description": "Tap to set",
								  "type": "DEVICE",
								  "required": "true",
								  "multiple": "false",
								  "capabilities": [
									"contactSensor"
								  ],
								  "permissions": [
									"r"
								  ]
								}
							  ]
							},
							{
							  "name": "Turn on/off this light...",
							  "settings": [
								{
								  "id": "lightSwitch",
								  "name": "Which switch?",
								  "description": "Tap to set",
								  "type": "DEVICE",
								  "required": "true",
								  "multiple": "false",
								  "capabilities": [
									"switch"
								  ],
								  "permissions": [
									"r",
									"x"
								  ]
								}
							  ]
							}
						  ]
						}
					  }
					}
		else:
			data = {'appId':'Not Recognized'}
			print('Page Unknown appId: %s' % content['appId'])
			
		return jsonify(data)

	elif (content['lifecycle'] == 'INSTALL'):
		print(content['lifecycle'])
		data = {'installData':{}}
		resp = content['installData']
		
		if content['appId'] == ST_WEBHOOK:
			print('Installing ST Webhook')
			deviceSubscription(resp['authToken'], resp['installedApp']['installedAppId'], resp['installedApp']['config']['contactSensor'][0]['deviceConfig']['deviceId'], 'contactSensor', 'contact', 'deviceSubscription')
		else:
			data = {'appId':'Not Recognized'}
			print('Install Unknown appId: %s' % content['appId'])
			
		return jsonify(data)

	elif (content['lifecycle'] == 'UPDATE'):
		print(content['lifecycle'])
		data = {'updateData':{}}
		resp = content['updateData']
		print('resp: %s' % resp)
		
		if content['appId'] == ST_WEBHOOK:
			print('Updating ST Webhook')
			deleteSubscriptions(resp['authToken'], resp['installedApp']['installedAppId'])
			deviceSubscription(resp['authToken'], resp['installedApp']['installedAppId'], resp['installedApp']['config']['contactSensor'][0]['deviceConfig']['deviceId'], 'contactSensor', 'contact', 'deviceSubscription')
		else:
			data = {'appId':'Not Recognized'}
			print('Update Unknown appId: %s' % content['appId'])
			
		return jsonify(data)

	elif (content['lifecycle'] == 'OAUTH_CALLBACK'):
		print(content['lifecycle'])
		data = {'oAuthCallbackData':{}}
		return jsonify(data)

	elif (content['lifecycle'] == 'EVENT'):
		data = {'eventData':{}}
		
		event = content['eventData']['events'][0]
		
		if content['appId'] == ST_WEBHOOK:
			print('eventData:\n%s' % content['eventData'])
			print('ST Webhook Config:\n%s' % content['eventData']['installedApp']['config'])
			
			authToken = content['eventData']['authToken']
			deviceId = content['eventData']['installedApp']['config']['lightSwitch'][0]['deviceConfig']['deviceId']

			if event['deviceEvent']['value'] == 'open':
				#turn on light
				setSwitch(authToken, deviceId, 'on')
			elif event['deviceEvent']['value'] == 'closed':
				#turn off light
				setSwitch(authToken, deviceId, 'off')
			else:
				print('Unrecognized event: %s' % event['deviceEvent']['value'])

		else:
			data = {'appId':'Not Recognized'}
			print('Event Unknown appId: %s' % content['appId'])
			
		return jsonify(data)

	elif (content['lifecycle'] == 'UNINSTALL'):
		print(content['lifecycle'])
		data = {'uninstallData':{}}
		return jsonify(data)

	else:
		print('Unknown Lifecycle: %s' % content['lifecycle'])
		return '',404

Notice this is written in a way that your server can act as the entry point for multiple SmartApps. In most lifecycles we check the appId to see if it’s our SmartApp. We could add checks for additional SmartApps if we choose to in the future.

  1. I’ll point out a few things within some of the lifecycles:
    • CONFIRMATION: We are simply grabbing the confirmationUrl passed in to us and making a GET Request back to that confirmationUrl. This allows SmartThings to verify that our URL is valid.
    • CONFIGURATION / INITIALIZE: We currently aren’t asking for any additional permissions outside of the specific devices we ask the user for in the PAGE phase. We’ll change that in a future post so we can get access to all events of interest for our SHD.
    • CONFIGURATION / PAGE: We only ask for READ permissions for the contact sensor, but we ask for READ/EXECUTE permissions for the switch. We just want to know about changes in the state of the contact sensor, but we want to execute commands to change the state of the switch. Also note that the id we assign inside the settings array will be given back to us within the EVENT lifecycle so we can navigate to the devices of interest.
    • INSTALL: Once the user completes the install, we need to subscribe to the events of interest so we can be notified when they occur. In this case, we are using a Device Subscription to only get the Open/Close events for the specific contact sensor device selected by the user. We will use the authToken passed to us in the json to execute this API.
    • UPDATE: When the user makes an update, we first want to delete previous subscriptions in case they have selected a new contact sensor. Then we perform the same Device Subscription that we did during the INSTALL for the new device.
    • EVENT: SmartThings will also pass an authToken to us as part of this lifecycle, which we can use to make subsequent API calls to achieve tasks that we have been given permissions for, such as turning on a specific switch that we have EXECUTE permissions for. So basically this token is only good for tasks that are initiated by subscribed events, which is perfect for this specific example. In a future post we’ll talk about how to execute tasks that are initiated outside of events, such as the user clicking in our SHD to turn on a switch.

Notice we also call three functions that we still need to add: deleteSubscriptions(), deviceSubscription() and setSwitch(). We’ll do that next.

  1. It’s time to add our missing functions:
#!/usr/bin/env python
 
#eventlet WSGI server
import eventlet
eventlet.monkey_patch()
 
#Flask Libs
from flask import Flask, abort, request, jsonify
 
#Web Sockets
from flask_socketio import SocketIO, send, emit, join_room, leave_room, disconnect, rooms
 
#HTTP Libs
import requests
 
#JSON Libs
import json
 
ST_WEBHOOK = '' # This will be filled in once we have an appId

def deleteSubscriptions(authToken, appID):
	baseURL = 'https://api.smartthings.com/v1/installedapps/'
	headers = {'Authorization': 'Bearer ' + authToken}
	endURL = '/subscriptions'
	fullURL = baseURL + str(appID) + endURL
	
	r = requests.delete(fullURL, headers=headers)
	print('delete all subscriptions: %s' % r.status_code)

def deviceSubscription(authToken, appID, deviceID, capability, attribute, subName):
	baseURL = 'https://api.smartthings.com/v1/installedapps/'
	headers = {'Authorization': 'Bearer ' + authToken}
	endURL = '/subscriptions'
	fullURL = baseURL + str(appID) + endURL
	
	datasub = {
		'sourceType':'DEVICE',
		'device': {
			'deviceId':deviceID,
			'componentId':'*',
			'capability':capability,
			'attribute':'*',
			'value':'*',
			'stateChangeOnly':True,
			'subscriptionName':subName
			}
		}
	r = requests.post(fullURL, headers=headers, json=datasub)
	print('Device Subscription: %d' % r.status_code)

def setSwitch(authToken, deviceID, state):
	baseURL = 'https://api.smartthings.com/v1/devices/'
	headers = {'Authorization': 'Bearer ' + authToken}
	endURL = '/commands'
	fullURL = baseURL + str(deviceID) + endURL

	datasub = {
		'commands': [ {
			'component':'main',
			'capability':'switch',
			'command':state
			}
		]
	}
	r = requests.post(fullURL, headers=headers, json=datasub)
	print('Set Switch to %s: %d' % (state, r.status_code))


app = Flask(__name__)

Here are the documentation references: Subscriptions; Devices

Now start your server and make sure it runs without error.

  1. Here’s the completed code:
#!/usr/bin/env python

#eventlet WSGI server
import eventlet
eventlet.monkey_patch()

#Flask Libs
from flask import Flask, abort, request, jsonify

#Web Sockets
from flask_socketio import SocketIO, send, emit, join_room, leave_room, disconnect, rooms

#HTTP Libs
import requests

#JSON Libs
import json

ST_WEBHOOK = '' # This will be filled in once we have an appId

def deleteSubscriptions(authToken, appID):
	baseURL = 'https://api.smartthings.com/v1/installedapps/'
	headers = {'Authorization': 'Bearer ' + authToken}
	endURL = '/subscriptions'
	fullURL = baseURL + str(appID) + endURL
	
	r = requests.delete(fullURL, headers=headers)
	print('delete all subscriptions: %s' % r.status_code)

def deviceSubscription(authToken, appID, deviceID, capability, attribute, subName):
	baseURL = 'https://api.smartthings.com/v1/installedapps/'
	headers = {'Authorization': 'Bearer ' + authToken}
	endURL = '/subscriptions'
	fullURL = baseURL + str(appID) + endURL
	
	datasub = {
		'sourceType':'DEVICE',
		'device': {
			'deviceId':deviceID,
			'componentId':'*',
			'capability':capability,
			'attribute':'*',
			'value':'*',
			'stateChangeOnly':True,
			'subscriptionName':subName
			}
		}
	r = requests.post(fullURL, headers=headers, json=datasub)
	print('Device Subscription: %d' % r.status_code)

def setSwitch(authToken, deviceID, state):
	baseURL = 'https://api.smartthings.com/v1/devices/'
	headers = {'Authorization': 'Bearer ' + authToken}
	endURL = '/commands'
	fullURL = baseURL + str(deviceID) + endURL

	datasub = {
		'commands': [ {
			'component':'main',
			'capability':'switch',
			'command':state
			}
		]
	}
	r = requests.post(fullURL, headers=headers, json=datasub)
	print('Set Switch to %s: %d' % (state, r.status_code))
	
app = Flask(__name__)
socketio = SocketIO(app)

@app.route('/', methods=['POST', 'GET'])
def smarthings_requests():
	if request.method == 'GET':
		return 'GET: OK'
	
	#return 'POST: OK'
	
	content = request.get_json()
	print('AppId: %s\nLifeCycle: %s' % (content['appId'], content['lifecycle']))
	
	if (content['lifecycle'] == 'PING'):
		print('PING: %s' % content)
		challenge = content['pingData']['challenge']
		data = {'pingData':{'challenge': challenge}}
		return jsonify(data)
		
	elif (content['lifecycle'] == 'CONFIRMATION'):
		confirmationURL = content['confirmationData']['confirmationUrl']
		r = requests.get(confirmationURL)
		print('CONFIRMATION\nContent: %s\nURL: %s\nStatus: %s' % (content,confirmationURL,r.status_code))
		if r.status_code == 200:
			return r.text
		else:
			abort(r.status_code)

	elif (content['lifecycle'] == 'CONFIGURATION' and content['configurationData']['phase'] == 'INITIALIZE'):
		print(content['configurationData']['phase'])
				
		if content['appId'] == ST_WEBHOOK:
			data = {
					  "configurationData": {
						"initialize": {
						  "name": "ST Test Webhook App",
						  "description": "ST Test Webhook App",
						  "id": "st_webhook_app_page_1",
						  "permissions": [
						  ],
						  "firstPageId": "1"
						}
					  }
					}
		else:
			data = {'appId':'Not Recognized'}
			print('Initialize Unknown appId: %s' % content['appId'])

		return jsonify(data)

	elif (content['lifecycle'] == 'CONFIGURATION' and content['configurationData']['phase'] == 'PAGE'):
		print(content['configurationData']['phase'])
		pageId = content['configurationData']['pageId']
		
		if content['appId'] == ST_WEBHOOK:
			data = {
					  "configurationData": {
						"page": {
						  "pageId": "1",
						  "name": "Select Devices",
						  "nextPageId": "null",
						  "previousPageId": "null",
						  "complete": "true",
						  "sections": [
							{
							  "name": "When this opens/closes...",
							  "settings": [
								{
								  "id": "contactSensor",
								  "name": "Which contact sensor?",
								  "description": "Tap to set",
								  "type": "DEVICE",
								  "required": "true",
								  "multiple": "false",
								  "capabilities": [
									"contactSensor"
								  ],
								  "permissions": [
									"r"
								  ]
								}
							  ]
							},
							{
							  "name": "Turn on/off this light...",
							  "settings": [
								{
								  "id": "lightSwitch",
								  "name": "Which switch?",
								  "description": "Tap to set",
								  "type": "DEVICE",
								  "required": "true",
								  "multiple": "false",
								  "capabilities": [
									"switch"
								  ],
								  "permissions": [
									"r",
									"x"
								  ]
								}
							  ]
							}
						  ]
						}
					  }
					}
		else:
			data = {'appId':'Not Recognized'}
			print('Page Unknown appId: %s' % content['appId'])
			
		return jsonify(data)

	elif (content['lifecycle'] == 'INSTALL'):
		print(content['lifecycle'])
		data = {'installData':{}}
		resp = content['installData']
		
		if content['appId'] == ST_WEBHOOK:
			print('Installing ST Webhook')
			deviceSubscription(resp['authToken'], resp['installedApp']['installedAppId'], resp['installedApp']['config']['contactSensor'][0]['deviceConfig']['deviceId'], 'contactSensor', 'contact', 'deviceSubscription')
		else:
			data = {'appId':'Not Recognized'}
			print('Install Unknown appId: %s' % content['appId'])
			
		return jsonify(data)

	elif (content['lifecycle'] == 'UPDATE'):
		print(content['lifecycle'])
		data = {'updateData':{}}
		resp = content['updateData']
		print('resp: %s' % resp)
		
		if content['appId'] == ST_WEBHOOK:
			print('Updating ST Webhook')
			deleteSubscriptions(resp['authToken'], resp['installedApp']['installedAppId'])
			deviceSubscription(resp['authToken'], resp['installedApp']['installedAppId'], resp['installedApp']['config']['contactSensor'][0]['deviceConfig']['deviceId'], 'contactSensor', 'contact', 'deviceSubscription')
		else:
			data = {'appId':'Not Recognized'}
			print('Update Unknown appId: %s' % content['appId'])
			
		return jsonify(data)

	elif (content['lifecycle'] == 'OAUTH_CALLBACK'):
		print(content['lifecycle'])
		data = {'oAuthCallbackData':{}}
		return jsonify(data)

	elif (content['lifecycle'] == 'EVENT'):
		data = {'eventData':{}}
		
		event = content['eventData']['events'][0]
		
		if content['appId'] == ST_WEBHOOK:
			print('eventData:\n%s' % content['eventData'])
			print('ST Webhook Config:\n%s' % content['eventData']['installedApp']['config'])
			
			authToken = content['eventData']['authToken']
			deviceId = content['eventData']['installedApp']['config']['lightSwitch'][0]['deviceConfig']['deviceId']

			if event['deviceEvent']['value'] == 'open':
				#turn on light
				setSwitch(authToken, deviceId, 'on')
			elif event['deviceEvent']['value'] == 'closed':
				#turn off light
				setSwitch(authToken, deviceId, 'off')
			else:
				print('Unrecognized event: %s' % event['deviceEvent']['value'])

		else:
			data = {'appId':'Not Recognized'}
			print('Event Unknown appId: %s' % content['appId'])
			
		return jsonify(data)

	elif (content['lifecycle'] == 'UNINSTALL'):
		print(content['lifecycle'])
		data = {'uninstallData':{}}
		return jsonify(data)

	else:
		print('Unknown Lifecycle: %s' % content['lifecycle'])
		return '',404
		
if __name__ == '__main__':
	socketio.run(app, debug=True, host='0.0.0.0', port=5000)


  1. We are now ready to setup our SmartApp in SmartThings Developer Workspace. We’ll cover that in the next post. See you there!

Leave a Comment

Your email address will not be published. Required fields are marked *