Connor NielsenDaniel Law
Created November 16, 2021

2021 PCL Home Automation Connor N.

Voice Activated Destiny 2 API Loadout Automation

Work in progress31
2021 PCL Home Automation Connor N.

Things used in this project

Hardware components

Raspberry Pi 3 Model B
Raspberry Pi 3 Model B
×1
Jumper wires (generic)
Jumper wires (generic)
×1
LED (generic)
LED (generic)
×1
Breadboard (generic)
Breadboard (generic)
×1

Software apps and online services

Raspbian
Raspberry Pi Raspbian
No explanation needed
Siri
Apple Siri
For Webhook implementation; replaceable
IFTTT Webhooks
Webhook
IFTTT-Mobile App
DIM
*Optional, but required for easier shortcut method*

Story

Read more

Schematics

Circuit

Rather simple, as was intended. Shouldn't need much explanation, it's just wiring to leds

Code

Server.py

Python
File containing most relevant code for the running Flask server. (NOTE: XXXXX values must be replaced manually as well as { } values, as both are just placeholders for private values)
import time
import urllib
import urllib.parse
from uuid import uuid4
import requests
import requests.auth
from flask import Flask, render_template, request, redirect, url_for, session
from Loadouts import updateCurrentEquippedStatus, equipLoadout

API_KEY = 'XXXXX'
HEADER = {'X-API-Key': API_KEY} ##apiKey definition

app = Flask(__name__)
app.secret_key = "XXXX"
o_session = requests.Session() ##flaskServer and session

@app.route('/') ##when server boots

@app.route('/authenticate') ##authentication request to API
def authenticate():
    state = make_authorization_url()
    state_params = {'state': state}
    url = 'https://www.bungie.net/en/OAuth/Authorize?client_id={clientID}&response_type=code&state=' + urllib.parse.urlencode(state_params)
    return render_template('authorization.html', url=url) ##goes to html

def make_authorization_url(): ##(direct copy) ##generates unique auth state
    state = str(uuid4())  ##random number
    session['state_token'] = state
    return state

@app.route('/callback/bungie') ##bungie callback for auth request, gets authCode
def callback():
    session.pop('state_token', None) ##clear the state from session
    code = request.args.get('code') ##get code from response
    print(code) 
    token = getToken(code)
    return redirect(url_for('authenticate')) ##goes back to first screen

def getToken(authCode): ##gets token from code, updates session 
    body = "https://www.bungie.net/platform/app/oauth/token/grant_type=authorization_code&code=" + authCode
    response = requests.post(body, headers={'Authorization' : API_KEY, 'Content-Type': 'application/x-www-form-urlencoded'})
    print(response)
    tokenJson = response.json()['Response']['accessToken']['value']
    o_session.headers["X-API-Key"] = API_KEY
    o_session.headers["Authorization"] = 'Bearer ' + str(tokenJson)
    return tokenJson

if __name__ == '__main__': 
    app.run(ssl_context='adhoc') ##opens server, must contain ssl






equipEventOccurred = False

import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(6, GPIO.OUT)
GPIO.setup(13, GPIO.OUT)
GPIO.setup(26, GPIO.OUT)

GPIO.output(5, GPIO.LOW)
GPIO.output(6, GPIO.LOW)
GPIO.output(13, GPIO.LOW)
GPIO.output(26, GPIO.LOW)

##led setup

while (not equipEventOccurred): ##loop unless an automated event has happened

    @app.route('/webhook', methods=['POST']) ##webhook get
    def webhook():
        if (request.method == "POST"):
            loadout = request.value1
            equipLoadout(loadout, o_session['Authorization'])

    equipStatus = updateCurrentEquippedStatus() ##gets current status of inventory
    GPIO.output(5, GPIO.LOW)
    GPIO.output(6, GPIO.LOW)
    GPIO.output(13, GPIO.LOW)
    GPIO.output(26, GPIO.LOW) ##lights only stay off if no loadout equipped

    if (equipStatus == "main"):
        GPIO.output(5, GPIO.HIGH)
    elif (equipStatus == "secondary"):
        GPIO.output(6, GPIO.HIGH)
    elif (equipStatus == "close"):
        GPIO.output(13, GPIO.HIGH)
    elif (equipStatus == "far"):
        GPIO.output(26, GPIO.HIGH)

    time.sleep(2)##checks webhook and loadouts every 2 seconds

Loadouts.py

Python
File for loadout based methods. Could be combined with Server to only have 1 file if you want, but Loadouts only contains method definitions and quite bulky ones at that, hence the other file
import requests


API_KEY = 'XXXXXXX'
HEADER = {'X-API-Key': API_KEY}

loadouts = { ##defines loadouts
    "main" : [6917529548693744090, 6917529266449208018, 6917529458574434359], ##[fatebringer, cartesian, 1k]
    "secondary" : [6917529212665616032, 6917529536711988303, 6917529304434337738], ##[DSC Shot, vex, rocket]
    "close" : [6917529373964997124, 6917529393574696592, 6917529210095335172], ##[grenade, smg, lament]
    "far" : [6917529498405921943, 6917529273453154914, 6917529545647977829] ##[scout, grenade, ghally]
}


def updateCurrentEquippedStatus(): ##checks current equipment to see if it fits a loadout

    getEquippedItems = requests.get("https://www.bungie.net/Platform/Destiny2/2/Profile/{destinyMembershipID}/Character/{characterID}/", headers=HEADER, params="components=205")
    jEquippedItems = getEquippedItems.json()

    mainCount = 0
    secondaryCount = 0
    closeCount = 0
    farCount = 0
    other = False

    for item in jEquippedItems['Response']['equipment']['data']['items']:
        if (item.get("location") == 1):
            if (item.get("itemHash") == loadouts.get("main")[0]):
                mainCount += 1
            elif (item.get("itemHash") == loadouts.get("secondary")[0]):
                secondaryCount += 1
            elif (item.get("itemHash") == loadouts.get("close")[0]):
                closeCount += 1
            elif (item.get("itemHash") == loadouts.get("far")[0]):
                farCount += 1
            else:
                break
        elif (item.get("location") == 2):
            if (item.get("itemHash") == loadouts.get("main")[1]):
                mainCount += 1
            elif (item.get("itemHash") == loadouts.get("secondary")[1]):
                secondaryCount += 1
            elif (item.get("itemHash") == loadouts.get("close")[1]):
                closeCount += 1
            elif (item.get("itemHash") == loadouts.get("far")[1]):
                farCount += 1
            else:
                break
        elif (item.get("location") == 3):
            if (item.get("itemHash") == loadouts.get("main")[2]):
                mainCount += 1
            elif (item.get("itemHash") == loadouts.get("secondary")[2]):
                secondaryCount += 1
            elif (item.get("itemHash") == loadouts.get("close")[2]):
                closeCount += 1
            elif (item.get("itemHash") == loadouts.get("far")[2]):
                farCount += 1
            else:
                break
    if (mainCount == 3):
        return "main"
    elif (secondaryCount == 3):
        return "secondary"
    elif (closeCount == 3):
        return "close"
    elif (farCount == 3):
        return "far"
    else:
        return "other"


def equipLoadout(loadout, auth_token): ##equips loadouts by sending with data

    AUTHHEADER = {
        HEADER,
        auth_token
    }
    jsonPack1 = {
        'membershipType' : 2,
        'itemId' : 6917529548693744090,
        'characterId' : XXXX
    }
    jsonPack2 = {
        'membershipType': 2,
        'itemId': 6917529304434337738,
        'characterId': XXX
    }
    jsonPack3 = {
        'membershipType': 2,
        'itemId': 6917529373964997124,
        'characterId': XXX
    }
    requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/", headers=HEADER + {"Authorization" : 'Bearer' + str(auth_token)}, data=jsonPack1)
    requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/", headers=AUTHHEADER, data=jsonPack2)
    requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/", headers=AUTHHEADER, data=jsonPack3)

    if (loadout == "main"):
        jsonPack1 = {
            'membershipType': 2,
            'itemId': 6917529548693744090,
            'characterId': XXX
        }
        jsonPack2 = {
            'membershipType': 2,
            'itemId': 6917529266449208018,
            'characterId': XXX
        }
        jsonPack3 = {
            'membershipType': 2,
            'itemId': 6917529458574434359,
            'characterId': XXX
        }
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack1)
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack2)
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack3)

    elif (loadout == "secondary"):
        jsonPack1 = {
            'membershipType': 2,
            'itemId': 6917529212665616032,
            'characterId': XXX
        }
        jsonPack2 = {
            'membershipType': 2,
            'itemId': 6917529536711988303,
            'characterId': XXX
        }
        jsonPack3 = {
            'membershipType': 2,
            'itemId': 6917529304434337738,
            'characterId': XXX
        }
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack1)
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack2)
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack3)

    elif (loadout == "close"):
        jsonPack1 = {
            'membershipType': 2,
            'itemId': 6917529373964997124,
            'characterId': XXX
        }
        jsonPack2 = {
            'membershipType': 2,
            'itemId': 6917529393574696592,
            'characterId': XXX
        }
        jsonPack3 = {
            'membershipType': 2,
            'itemId': 6917529210095335172,
            'characterId': XXX
        }
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack1)
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack2)
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack3)

    elif (loadout == "far"):
        jsonPack1 = {
            'membershipType': 2,
            'itemId': 6917529498405921943,
            'characterId': XXX
        }
        jsonPack2 = {
            'membershipType': 2,
            'itemId': 6917529273453154914,
            'characterId': XXX
        }
        jsonPack3 = {
            'membershipType': 2,
            'itemId': 6917529545647977829,
            'characterId': XXX
        }
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack1)
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack2)
        requests.post("https://www.bungie.net/Platform/Destiny2/Actions/Items/EquipItem/",
                      headers=AUTHHEADER, data=jsonPack3)

authorization.html

HTML
The auth code request cannot be fully automatic, therefore this hyperlink satifsifies the security needs
<html>
    <div>
        <a href="{{ url }}"> Authentication </a>
    </div>
</html>

idAndHashGrabbers.py

Python
API Querys used to get the data values needed for other parts. Will never run in actual code as its use is only during programming. The programming equivelent of my scratch paper.
import requests

basePath = "https://www.bungie.net/Platform"
API_KEY = 'XXXXX'
HEADER = {'X-API-Key': API_KEY}

dMemIdGet = requests.get("https://www.bungie.net/Platform/User/GetMembershipsById/{ID}/{IDTYPE}/", headers=HEADER)

print(dMemIdGet.json())

destinyMembershipId = "XXXXX"
## REQUEST VALUE IDS: ProfileInventories = 102, Characters = 200, CharacterInventories = 201, CharacterEquipment = 205
## ItemInstances = 300

profileInfoGet = requests.get("https://www.bungie.net/Platform/Destiny2/2/Profile/{destinyMembershipID}/", headers=HEADER, params="components=200")

print(profileInfoGet.json())


itemTestGet = requests.get("https://www.bungie.net/Platform/Destiny2/2/Profile/{destinyMembershipID}/Item/{characterID}/", headers=HEADER, params="components=300")

print(itemTestGet.json())

getEquippedItems = requests.get("https://www.bungie.net/Platform/Destiny2/2/Profile/{destinyMembershipID}/Character/{characterID}/", headers=HEADER, params="components=205")
print(getEquippedItems.json())

Credits

Connor Nielsen
1 project • 0 followers
Daniel Law
47 projects • 10 followers
Teacher. Maker. Citizen of the planet.
Thanks to Helpful Website and IFTTT.

Comments