├── .gitignore ├── deploy.sh ├── notifications.py ├── piggydash.py ├── readme.md ├── requirements.txt ├── secrets.example.yml └── simple.py /.gitignore: -------------------------------------------------------------------------------- 1 | secrets.yml 2 | *.pyc 3 | *.zip 4 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir="$(pwd)" 4 | 5 | zip -ru piggydash.zip ./*.py secrets.yml 6 | 7 | source ~/.virtualenvs/piggydash/bin/activate 8 | pip install -r requirements.txt 9 | deactivate 10 | 11 | pushd ~/.virtualenvs/piggydash/lib/python2.7/site-packages 12 | zip -ru $dir/piggydash.zip ./* 13 | popd 14 | 15 | aws lambda update-function-code \ 16 | --profile=bjacobel \ 17 | --function-name piggydash \ 18 | --zip-file fileb://$dir/piggydash.zip 19 | -------------------------------------------------------------------------------- /notifications.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | def push_notify(key, amount, goal): 4 | requests.post( 5 | "https://maker.ifttt.com/trigger/piggydash/with/key/{key}".format(key=key), 6 | json={ 7 | "value1": "%.2f" % amount, 8 | "value2": goal 9 | } 10 | ) 11 | -------------------------------------------------------------------------------- /piggydash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from yaml import load 4 | from os import path 5 | from addict import Dict 6 | import simple 7 | import notifications 8 | 9 | def parse_yaml(): 10 | settings = None 11 | 12 | if not path.isfile("./secrets.yml"): 13 | raise Exception("Missing secrets.yml") 14 | else: 15 | with open("./secrets.yml", "rb") as f: 16 | settings = Dict(load(f)) 17 | 18 | if ( 19 | not settings.simple.username or 20 | not settings.simple.password or 21 | not settings.simple.goal_name or 22 | not settings.simple.transfer_amount 23 | ): 24 | raise Exception("Missing required secret keys/config, check secrets.example.yml for instructions") 25 | else: 26 | return settings 27 | 28 | 29 | def main(event=None, context=None): 30 | settings = parse_yaml() 31 | 32 | sp = simple.Simple(settings.simple) 33 | 34 | sp.login() 35 | sp.goal_lookup() 36 | 37 | success = sp.transact() 38 | 39 | # IFTTT maker notification is optional -- don't do this if the credentials are not present 40 | if settings.ifttt.key and success: 41 | notifications.push_notify(settings.ifttt.key, settings.simple.transfer_amount, settings.simple.goal_name) 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ##Piggydash 2 | --- 3 | 4 | ###Objectives 5 | Use the [Amazon IoT Dash button](https://aws.amazon.com/iotbutton/) as a modern day piggybank. Uses the [Simple](https://simple.com) API to transfer money out of my checking account into a rainy day fund. 6 | 7 | Inspired by [Ted Benson's Medium post](https://medium.com/@edwardbenson/how-i-hacked-amazon-s-5-wifi-button-to-track-baby-data-794214b0bdd8) on his Dash hack. 8 | 9 | Originally this project ran on a normal Dash button and a local Raspberry Pi, but since the IoT Dash Button was released I've moved it to AWS Lambda. If you want to run it yourself you can deploy it out to your own Lambda and link the IoT button as a trigger, or potentially use a normal Dash button to send an HTTP request to API Gateway and trigger the Lambda that way, if you've hacked your v1 Dash button to send custom HTTP requests. 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | addict==2.0.0 2 | beautifulsoup4==4.4.0 3 | pyyaml==3.11 4 | requests==2.7.0 5 | -------------------------------------------------------------------------------- /secrets.example.yml: -------------------------------------------------------------------------------- 1 | ifttt: 2 | key: # key of your IFTTT Maker endpoint (https://maker.ifttt.com/use/) (optional) 3 | simple: 4 | username: # Your Simple username (required) 5 | password: # Your Simple password (required) 6 | goal_name: # Name of a Simple goal to put money towards (required) 7 | transfer_amount: # Amount of money to transfer to Simple goal in dollars (required, use number, not string) 8 | -------------------------------------------------------------------------------- /simple.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import requests 3 | 4 | class Simple: 5 | def __init__(self, simple_settings): 6 | self.session = requests.Session() 7 | 8 | self.session.headers.update({ 9 | "User-Agent": ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) " + 10 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36") 11 | }) 12 | 13 | self.username = simple_settings.username 14 | self.password = simple_settings.password 15 | self.goal_name = simple_settings.goal_name 16 | self.transfer_amount = simple_settings.transfer_amount 17 | 18 | 19 | def login(self): 20 | login_page = self.session.get("https://bank.simple.com/signin") 21 | login_content = login_page.content 22 | 23 | login_csrf = BeautifulSoup(login_content, 'html.parser').find('input', {"name": "_csrf"})['value'] 24 | 25 | response = self.session.post( 26 | "https://bank.simple.com/signin", 27 | data = { 28 | "username": self.username, 29 | "password": self.password, 30 | "_csrf": login_csrf 31 | }, 32 | headers = { 33 | "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", 34 | } 35 | ) 36 | 37 | if not response.ok: 38 | raise Exception("Something happened with Simple authentication.") 39 | 40 | 41 | def get_csrf(self): 42 | try: 43 | self.csrf 44 | return 45 | except AttributeError: 46 | goals_page = self.session.get("https://bank.simple.com/goals") 47 | 48 | self.csrf = BeautifulSoup(goals_page.content, 'html.parser').find('meta', {"name":"_csrf"})['content'] 49 | 50 | 51 | def goal_lookup(self): 52 | self.get_csrf() 53 | 54 | response = self.session.get( 55 | url = "https://bank.simple.com/goals/data", 56 | headers = { 57 | "X-Request": "JSON", 58 | "X-CSRF-Token": self.csrf 59 | } 60 | ) 61 | 62 | for goal in response.json(): 63 | if goal['name'] == self.goal_name and not goal['archived']: 64 | self.goal_id = goal['id'] 65 | return 66 | 67 | raise Exception("No goal matching specified name found") 68 | 69 | 70 | def transact(self): 71 | self.get_csrf() 72 | 73 | response = self.session.post( 74 | url = "https://bank.simple.com/goals/{}/transactions".format(self.goal_id), 75 | headers = { 76 | "X-Requested-With": "XMLHttpRequest", 77 | "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", 78 | }, 79 | data = { 80 | "amount": int(self.transfer_amount * 10000), 81 | "_csrf": self.csrf, 82 | } 83 | ) 84 | 85 | return response.ok 86 | --------------------------------------------------------------------------------