From 6ed73100bd8a4f2ec59293aa8f5cea4ff67646c0 Mon Sep 17 00:00:00 2001 From: malaow3 Date: Sat, 26 Sep 2020 13:53:45 -0400 Subject: [PATCH] Add files --- README.md | 25 ++++++++ bggraph.js | 36 ++++++++++++ bggraph2.js | 31 ++++++++++ keep_alive.py | 127 ++++++++++++++++++++++++++++++++++++++++ main.py | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 377 insertions(+) create mode 100644 README.md create mode 100644 bggraph.js create mode 100644 bggraph2.js create mode 100644 keep_alive.py create mode 100644 main.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b920bc --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Glucose-Widget +Continuous Glucose Monitor iOS 14 Widget setup using Python and JS + +As per Scriptable docs, "the widget will refresh periodically and the rate at which the widget refreshes is largely determined by the operating system". My experience has been the widget will update within a new glucose reading (~5 minute window), but will not update as frequently with low power mode active. + +# Setup +* Install Scriptable on iOS 14 device (https://scriptable.app) +* Copy repository to local machine +* Sign up for Repl.it account (https://repl.it/) for webhosting +* Create a new Repl with python +* Copy contents of main.py and keep_alive.py to repl +* Add .env file containing username and password –– format should be: +```python + username="myusername" + password="mypassword" +``` +* Run repl and copy link of webpage that is set up +* _Optional:_ set up account on uptimerobot (http://uptimerobot.com) to ping repl site to ensure it is constantly active +* Copy contents of Bggraph.js and Bggraph2.js to scriptable and modify the appropriate urls +* Add widget to home screen + +Note: Repl.it web hosting is optional, this can be hosted anywhere that supports python and Flask —— I chose Repl since it is free + +# Widget + diff --git a/bggraph.js b/bggraph.js new file mode 100644 index 0000000..e16a631 --- /dev/null +++ b/bggraph.js @@ -0,0 +1,36 @@ +/* + file: bggraph.js + fileOverview: Large widget script +*/ + +// Replace url with your repl link +const url = "https://example.myaccount.repl.co" +let req = new Request(url) +let res = await req.loadJSON() + +if (config.runsInWidget) { + let widget = await createWidget(res.bg, "#212121") + Script.setWidget(widget) + Script.complete() +} + +async function createWidget(pretitle, color) { + let w = new ListWidget() + w.backgroundColor = new Color(color) + let item = " " + pretitle + let preTxt = w.addText(item) + preTxt.textColor = Color.white() + preTxt.centerAlignText() + preTxt.font = Font.systemFont(42) + + w.addSpacer(1) + let imgReq = new Request("https://example.myaccount.repl.co/plot.png") + let img = await imgReq.loadImage() + let wimg = w.addImage(img) + wimg.rightAlignImage() + wimg.imageSize = new Size(450, 255) + w.centerAlignContent + w.setPadding(0, 0, 0, 0) + + return w +} \ No newline at end of file diff --git a/bggraph2.js b/bggraph2.js new file mode 100644 index 0000000..e990cf4 --- /dev/null +++ b/bggraph2.js @@ -0,0 +1,31 @@ +/* + file: bggraph2.js + fileOverview: Medium widget script +*/ +// Replace url with your repl link +const url = "https://example.myaccount.repl.co" +let req = new Request(url) +let res = await req.loadJSON() + +if (config.runsInWidget) { + let widget = await createWidget(res.bg, "#212121") + Script.setWidget(widget) + Script.complete() +} + +async function createWidget(pretitle, color) { + let w = new ListWidget() + w.backgroundColor = new Color(color) + let item = " " + pretitle + let preTxt = w.addText(item) + preTxt.textColor = Color.white() + preTxt.font = Font.systemFont(32) + + let imgReq = new Request("https://example.myaccount.repl.co/plot2.png") + let img = await imgReq.loadImage() + let wimg = w.addImage(img) + wimg.centerAlignImage() + wimg.imageSize = new Size(360, 90) + + return w +} \ No newline at end of file diff --git a/keep_alive.py b/keep_alive.py new file mode 100644 index 0000000..a35758c --- /dev/null +++ b/keep_alive.py @@ -0,0 +1,127 @@ +''' +file: keep_alive.py +fileOverview: Webserver backend to display data +''' +from flask import Flask, render_template +from threading import Thread +from flask import jsonify +import json +import io +from flask import Response +from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas +from matplotlib.figure import Figure + +app = Flask('') + + +@app.route('/plot.png') +def plot_png(): + fig = create_figure() + fig.patch.set_facecolor('#212121') + output = io.BytesIO() + FigureCanvas(fig).print_png(output) + return Response(output.getvalue(), mimetype='image/png') + + +def create_figure(): + text_file = open("datalist.txt", "r") + lines = text_file.read().split(',') + lines = [int(i) for i in lines] + print(lines) + fig = Figure() + axis = fig.add_subplot(1, 1, 1) + xs = range(11) + ys = lines + axis.axis(ymin=40, ymax=400) + axis.spines['bottom'].set_color('#FFFFFF') + axis.spines['top'].set_color('#FFFFFF') + axis.spines['right'].set_color('#FFFFFF') + axis.spines['left'].set_color('#FFFFFF') + + axis.tick_params(axis='x', colors='#FFFFFF') + axis.tick_params(axis='y', colors='#FFFFFF') + + axis.yaxis.label.set_color('#FFFFFF') + axis.xaxis.label.set_color('#FFFFFF') + axis.set_facecolor("#212121") + axis.tick_params( + axis='x', + which='both', + bottom=False, + top=False, + labelbottom=False) + axis.tick_params( + axis='y', + which='both', + labelsize=16) + axis.scatter(xs, ys, s=100) + return fig + + +@app.route('/plot2.png') +def plot_png2(): + fig = create_figure2() + fig.patch.set_facecolor('#212121') + output = io.BytesIO() + FigureCanvas(fig).print_png(output) + return Response(output.getvalue(), mimetype='image/png') + + +def create_figure2(): + text_file = open("datalist.txt", "r") + lines = text_file.read().split(',') + lines = [int(i) for i in lines] + print(lines) + fig = Figure(figsize=(16, 4)) + axis = fig.add_subplot(1, 1, 1) + xs = range(11) + ys = lines + axis.axis(ymin=40, ymax=400) + axis.spines['bottom'].set_color('#FFFFFF') + axis.spines['top'].set_color('#FFFFFF') + axis.spines['right'].set_color('#FFFFFF') + axis.spines['left'].set_color('#FFFFFF') + + axis.tick_params(axis='x', colors='#FFFFFF') + axis.tick_params(axis='y', colors='#FFFFFF') + + axis.yaxis.label.set_color('#FFFFFF') + axis.xaxis.label.set_color('#FFFFFF') + axis.set_facecolor("#212121") + axis.tick_params( + axis='x', + which='both', + bottom=False, + top=False, + labelbottom=False) + axis.tick_params( + axis='y', + which='both', + labelsize=26) + axis.scatter(xs, ys, s=300) + + return fig + + +@app.route("/img") +def img(): + return render_template("img.html") + + +@app.route('/') +def home(): + f = open("output.txt", "r") + data = json.loads(f.read()) + return jsonify(data) + + +def run(): + try: + app.run(host='0.0.0.0', port=8080) + except: # noqa: E722 + pass + + +def keep_alive(): + t = Thread(target=run) + t.start() diff --git a/main.py b/main.py new file mode 100644 index 0000000..98813a8 --- /dev/null +++ b/main.py @@ -0,0 +1,158 @@ +''' +file: main.py +fileOverview: Scrape Glucose Data and output contents to text file + and create thread for webserver +''' +import urllib.request +import urllib.error +import urllib.parse +import json +import os +import time +from keep_alive import keep_alive + + +def get_data(): + """ + Scrapes Glucose Data and returns results + + Returns: + (String, String, List): Tuple containing glucose value, trend value, and list of glucose data + """ + method = "POST" + handler = urllib.request.HTTPHandler() + opener = urllib.request.build_opener(handler) + sessionIdUrl = 'https://share2.dexcom.com/ShareWebServices/Services/General/LoginPublisherAccountByName' + glucoseUrl = 'https://share2.dexcom.com/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues?sessionID=' # noqa:E501 + username = os.getenv("username") + password = os.getenv("password") + glucoseGetParams = '&minutes=1440&maxCount=11' + payload = {"password": password, "applicationId": "d89443d2-327c-4a6f-89e5-496bbb0317db", "accountName": username} + payload = json.dumps(payload).encode('utf8') + seshRequest = urllib.request.Request(sessionIdUrl, payload) + seshRequest.add_header("Content-Type", 'application/json') + seshRequest.add_header("User-Agent", 'Dexcom Share/3.0.2.11 CFNetwork/672.0.2 Darwin/14.0.0') + seshRequest.add_header("Accept", 'application/json') + seshRequest.get_method = lambda: method + sessionID = None + try: + connection = opener.open(seshRequest) + except urllib.error.HTTPError as e: + connection = e + if connection.code == 200: + sessionID = connection.read() + sessionID = sessionID[1:-1] + sessionID = sessionID.decode("utf8") + else: + print((connection.code)) + + getGlucoseUrl = glucoseUrl + sessionID + glucoseGetParams + glucoseRequest = urllib.request.Request(getGlucoseUrl) + glucoseRequest.get_method = lambda: method + glucoseRequest.add_header("Accept", 'application/json') + glucoseRequest.add_header("Content-Length", '0') + emptyLoad = {"": ""} + try: + connection2 = opener.open(glucoseRequest, json.dumps(emptyLoad).encode("utf8")) + except urllib.error.HTTPError as e: + connection2 = e + + glucose = None + trend = None + data = [] + + if connection2.code == 200: + glucoseReading = connection2.read() + glucoseReading = json.loads(glucoseReading) + glucose = glucoseReading[0]["Value"] + trend = glucoseReading[0]["Trend"] + data = [] + for item in glucoseReading: + data.append(int(item["Value"])) + + else: + print((connection2.code)) + + return (glucose, trend, data) + + +def replace_trend(trend): + """ + Replace trend number with arrows + + Args: + trend (int): trend number + + Returns: + String: trend represented with arrows + """ + trendtext = None + if trend == 0: + trendtext = "" + if trend == 1: + # trendtext = "rising quickly" + trendtext = "↑↑" + if trend == 2: + # trendtext = "rising" + trendtext = "↑" + if trend == 3: + # trendtext = "rising slightly" + trendtext = "↗" + if trend == 4: + # trendtext = "steady" + trendtext = "→" + if trend == 5: + # trendtext = "falling slightly" + trendtext = "↘" + if trend == 6: + # trendtext = "falling" + trendtext = "↓" + if trend == 7: + trendtext = "↓↓" + if trend == 8: + # trendtext = "unable to determine a trend" + trendtext = " " + if trend == 9: + # trendtext = "trend unavailable" + trendtext = " " + return trendtext + + +glucose, trend, datalist = get_data() +trend_str = replace_trend(trend) + +final_string = f"{glucose}{trend_str}" +# final_string = f"{glucose}" +data = {"bg": final_string} +f = open("output.txt", "w") +json.dump(data, f) +f.close() +with open('datalist.txt', 'w') as f: + for counter in range(len(datalist)): + item = datalist[counter] + if counter != len(datalist)-1: + f.write(f"{item},") + else: + f.write(f"{item}") + +keep_alive() + +try: + while 1: + glucose, trend, datalist = get_data() + trend_str = replace_trend(trend) + final_string = f"{glucose}{trend_str}" + data = {"bg": final_string} + f = open("output.txt", "w") + json.dump(data, f) + f.close() + with open('datalist.txt', 'w') as f: + for counter in range(len(datalist)): + item = datalist[counter] + if counter != len(datalist)-1: + f.write(f"{item},") + else: + f.write(f"{item}") + time.sleep(30) +except KeyboardInterrupt: + f.close()