SmartThings Community

Python 3 & Flask Webhook Automation


(Brendan Cain) #1

Hello everyone,

For anyone looking to write a webhook automation in python3/flask please see this template below. After fumbling through all the different documentation and dissecting the bad examples in the docs, and reading some super helpful community posts, I finally came up with this. You should literally be able to copy paste it, start up Ngrok, and self publish.

The issue I was having was that the life cycle documentation is unclear if you are following the basic how to. It acts like you should be able to get to the self publish test as long as your app returns status code 200. I couldn’t get my public key until i started returning the challenge code posted by smartthings.

Unfortunately the “how to” is before the lifecycle documentation in the docs.

Here are the helpful links I used

How To:
https://smartthings.developer.samsung.com/develop/getting-started/automation.html

Life Cycle Basics (You need to have the ping response working to get past the first part of the how to)
https://smartthings.developer.samsung.com/develop/guides/smartapps/lifecycles.html

Detailed listing of lifecycle configurations
https://smartthings.developer.samsung.com/develop/api-ref/smartapps-v1.html#

Helpful community post response by Jim Anderson @Jim

It’s a super basic template that does the same example (sort of) that they do in the docs. It does handle all the life cycles, so you can just change them. You may also want to move the header verification before the “PING” IF statement after you get your public key. Thought I would share this since it took me a while, and might save someone else some time :stuck_out_tongue:

# Brendan Cain 2018
# brendancain99 at gmail.com

#Lifecycle Documentation (3 sources)

#from the developer guide page
#https://smartthings.developer.samsung.com/develop/guides/smartapps/lifecycles.html

#from the api page
#https://smartthings.developer.samsung.com/develop/api-ref/smartapps-v1.html#

#Jim anderson - because he is morehelpful than most the documentation :p
#https://community.smartthings.com/t/automation-webhook-asp-net-core/144749


from httpsig.verify import HeaderVerifier, Verifier
from flask import Flask, request, jsonify

app = Flask(__name__)


def get_public_key():
    with open('smartthings_rsa.pub', 'r') as f:
        return f.read()


@app.route('/', methods=['POST'])
def smarthings_requests():
    content = request.get_json()

    print(content)
    if (content['lifecycle'] == 'PING'):
        print("PING")
        challenge = content['pingData']["challenge"]
        data = {'pingData':{'challenge': challenge}}
        return jsonify(data)


    hv = HeaderVerifier(headers=request.headers, secret=get_public_key(), method='POST', path='/')

    if not (hv.verify()):
        # Invalid signature, return 403
        return '', 403


    if (content['lifecycle'] == 'CONFIGURATION' and content['configurationData']['phase'] == 'INITIALIZE'):
        print(content['configurationData']['phase'])
        data = {
                  "configurationData": {
                    "initialize": {
                      "name": "Webhook App Cain",
                      "description": "Webhook App Cain",
                      "id": "cain_webhook_app_page_1",
                      "permissions": [
                        # "l:devices"
                      ],
                      "firstPageId": "1"
                    }
                  }
                }
        return jsonify(data)

    elif (content['lifecycle'] == 'CONFIGURATION' and content['configurationData']['phase'] == 'PAGE'):
        print(content['configurationData']['phase'])
        pageId = content['configurationData']['pageId']

        data = {
                  "configurationData": {
                    "page": {
                      "pageId": "1",
                      "name": "On When Open/Off When Shut WebHook App",
                      "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"
                              ]
                            }
                          ]
                        }
                      ]
                    }
                  }
                }
        return jsonify(data)


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


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


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

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


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


    else:
        return '',404



if __name__ == '__main__':
    app.run('0.0.0.0',debug=True)

(Jim Anderson) #2

Awesome!

I’ve passed this feedback on to the documentation team as well, so they can consider ways to improve the pain points you went through.