SmartThings Smart Home Dashboard #4

So far we’ve established a public URL to access our server, built out our basic SmartApp framework within our server, configured our SmartApp in SmartThings Developer Workspace, installed our SmartApp, and hopefully was able to control a switch with a contact sensor. By now you can probably see the potential.

This time I’m going to show you how you can access devices that weren’t explicitly configured during the installation of your SmartApp. We will request READ access to ALL devices during the installation, and then we can simply subscribe the the capabilities that we are interested in. For example, in the last post we asked the user to select a specific contact sensor and then we subscribed to the events related to that specific device. Once we received an open or closed event, we executed an on or off command on the specific switch device that the user selected. If we request READ permission for all devices, then we can simply subscribe to the contactSensor capability and the associated contact attribute and we can see ALL contactSensor events that occur within our hub location.

For this post, we are going to leave our current configuration in place which asks the user to select a contact sensor and a switch, but we are going to request READ permissions for all devices and will subscribe to the switch capability and the associated switch attribute. That will allow us to get notified anytime a switch, any switch in our location, is turned on or off. We’ll then use web sockets to send those events to a test HTML page so we can see them in real-time (or at least see them when they are sent to our server).

Getting Started

  1. First, start your server and then start your Ngrok tunnel, if you haven’t already, and update your Target URL for your project in SmartThings Developer Workspace. Remember, this will make a CONFIRMATION call to your server, so your server and your tunnel must be running.
  1. Now we’ll update our server to add the needed code. This is the code in it’s entirety with the changes highlighted:
#!/usr/bin/env python

#eventlet WSGI server
import eventlet
eventlet.monkey_patch()

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

#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 = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' # SmartThings 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 capabilitySubscription(authToken, locationID, appID, capability, attribute, subName, stateChangeOnly=True):
	baseURL = 'https://api.smartthings.com/v1/installedapps/'
	headers = {'Authorization': 'Bearer ' + authToken}
	endURL = '/subscriptions'
	fullURL = baseURL + str(appID) + endURL

	datasub = {
		'sourceType':'CAPABILITY',
		'capability': {
			'locationId':locationID,
			'capability':capability,
			'attribute':attribute,
			'value':'*',
			'stateChangeOnly':stateChangeOnly,
			'subscriptionName':subName
			}
		}
	r = requests.post(fullURL, headers=headers, json=datasub)
	print('Capability Subscription [%s / %s]: %d' % (capability, attribute, 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))
	
def getDeviceDetails(authToken, deviceID):
	#https://api.smartthings.com/v1/devices/{deviceId}
	baseURL = 'https://api.smartthings.com/v1/devices/'
	headers = {'Authorization': 'Bearer ' + authToken}
	fullURL = baseURL + str(deviceID)
	r = requests.get(fullURL, headers=headers)
	print('Get Device Details: %d' % (r.status_code))
	print('Details:\n%s' % r.text)
	if r.status_code == 200:
		details = json.loads(r.text)
		return details['label']
	return 'Error'
	
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins=['http://localhost:5000','http://192.168.2.221:5000','http://b016dcd112aa.ngrok.io','https://b016dcd112aa.ngrok.io'])
app.config['SECRET_KEY'] = 'my_super_secret_key'

@socketio.on('connect')
def socket_connect():
	data = json.dumps({'status': 'connected'})
	emit('conn', data, broadcast=False)


@app.route('/', methods=['POST', 'GET'])
def smarthings_requests():
	if request.method == 'GET':
		#return 'GET: OK'
		return render_template('shd.html')
	
	#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": [
							"r:devices:*"
						  ],
						  "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')
			capabilitySubscription(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'switch', 'switch', 'capSwitchSubscription')
		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')
			capabilitySubscription(resp['authToken'], resp['installedApp']['locationId'], resp['installedApp']['installedAppId'], 'switch', 'switch', 'capSwitchSubscription')
		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'])
			deviceLabel = getDeviceDetails(authToken, event['deviceEvent']['deviceId'])
			print('%s Changed to: %s' % (deviceLabel, event['deviceEvent']['value']))
			socketio.emit('event', json.dumps({'device':deviceLabel, 'state': 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

@app.route('/apple-touch-icon-152x152.png')
@app.route('/apple-touch-icon-152x152-precomposed.png')
@app.route('/apple-touch-icon-120x120-precomposed.png')
@app.route('/apple-touch-icon-120x120.png')
@app.route('/apple-touch-icon-precomposed.png')
@app.route('/apple-touch-icon.png')
@app.route('/favicon.ico')
def favicon():
	print('favicon')
	return send_from_directory('/home/pi/static', 'favicon.png')
		
if __name__ == '__main__':
	socketio.run(app, debug=True, host='0.0.0.0', port=5000)

I think most of this should make sense in that it’s primarily just variations of what we did previously (such as the Capability Subscription and getting the Device Details). Feel free to add other Capability subscriptions as well. Just remember to give each one a unique subscriptionName.

After making changes to the UPDATE lifecycle, you will need to go back into the SmartThings mobile app and update your SmartApp to execute that code. Even if you don’t change any values, you still need to click through the screens.

This might be the first unrecognized change:

socketio = SocketIO(app, cors_allowed_origins=[‘http://localhost:5000′,’http://192.168.2.221:5000′,’http://b016dcd112aa.ngrok.io’,’https://b016dcd112aa.ngrok.io’])

Obviously you would need to change the URLs to match your own, but this will allow our flask-socketio web socket to work over both HTTP and HTTPS, but only from those explicit URLs. That means you can run your dashboard over the public HTTPS URL.

The last new thing wasn’t really necessary, but I prefer to have a favicon to display in my browser, so I included a route for that as well. Here is the favicon.png image if you want to save it and copy it to the Pi.

favicon.png
  1. Now let’s create the shd.html file. You’ll want to place this in the /home/pi/st_webhook/templates folder.
<!DOCTYPE html>
<html>

<head>
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta name="apple-mobile-web-app-capable" content="yes">	
	<title>SmartThings Events</title>
	<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/4.1.3/socket.io.min.js"></script>
	<style>
	body {
	  background-color: lightblue;
	  color: blue;
	  font-size: 24px;
	}
	</style>
</head>

<body>
  <h1>SmartThings Events</h1>
  <p id="events"></p>

<script>
	var DOW_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

	function getTimeDisplay(dt) {
		var time_str = "";
		var hrs = "";
		var mins = "";
		var ap = "AM";
		var val = 0;
		
		val = dt.getHours();
		if (val >=12) {
			ap = "PM";
			if (val > 12) {
				val -= 12;
			}
		}
		else if (val == 0) {
			val = 12;
		}
		hrs = val.toString();
		
		val = dt.getMinutes();
		mins = ("0" + val).slice(-2);
		
		time_str = hrs + ":" + mins + " " + ap;
		return time_str;
	}

	var protocol = window.location.protocol;
	var socket = io.connect(protocol + '//' + document.domain + ':' + location.port, {
		reconnection: true,
		reconnectionDelay: 1000,
		reconnectionDelayMax: 10000,
		reconnectionAttempts: 99999
	});

	socket.on('conn', function(msg) {
		var dt = new Date();
		var dtDisp = DOW_SHORT[dt.getDay()] + " " + getTimeDisplay(dt);
		var data = JSON.parse(msg);
		console.log(dtDisp + " - " + data.status);
	});

	socket.on('event', function(msg) {
		console.log("event: " + msg);
		var dt = new Date();
		var dtDisp = DOW_SHORT[dt.getDay()] + " " + getTimeDisplay(dt);
		var data = JSON.parse(msg);
		var disp = dtDisp + " - " + data.device + ": " + data.state;
		document.querySelector("#events").innerHTML = document.querySelector("#events").innerHTML + disp + "<br />";
	});

</script>
</body>
</html>

You can see it’s nothing fancy, but I wanted to give you a way to see what’s happening without watching your server or your Ngrok Dashboard, which of course you certainly can and should do as well until you’re comfortable with the output.

The one thing I wanted to point out here is the line I have highlighted. If you have an iOS device, this line will make the browser appear to be an app if you save it to your home screen. You don’t see any of the browser controls when you open it from the shortcut, which maximizes the display area. This is more useable once you have a stable version and not making updates to the HTML, but once you do you’ll love it!

Notice when you test that not only will you see the events from your contact sensor and selected switch, but you will see the events from all switches in your hub location.

  1. Now restart your server if necessary after making these changes and start testing! Make sure you check your server for any errors (you can start removing some of the print statements to clean up the output), and check your Ngrok Dashboard to see what’s being sent in/out of your server. You should see something like this:
SmartThings Events with Developer Tools Selected to see the Console Logs

Note that Bedroom Temp above is in fact a contact sensor (actually a multi-purpose sensor), I just happen to use it as a temperature sensor normally.

  1. This might be a good time to play around a little with what you have so far to get comfortable with the various APIs. Subscribe to different capabilities (such as the presenceSensor, or lock, or whatever devices you have). You can also try sending commands to other devices you have that will accept commands (switch, lock, door, thermostat, etc.).
  1. When you’re ready, check out the next post where we will learn how to get a list of all of our devices, their current state, and which room they belong to. Here’s the link! See you there!

1 thought on “SmartThings Smart Home Dashboard #4”

Leave a Comment

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