-
-
Save donfelipo/69cf704362f35c7ca5625f2e33893e24 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: deep-green; icon-glyph: eye; | |
let location = "1"; | |
let param = args.widgetParameter; | |
if (param != null && param.length > 0) { | |
location = param; | |
} | |
// TODO embed https://www.mysports.com/nox/public/v1/studios/1225816400/utilization/v2/indicator/limits with different color scheme | |
// utilizationIndicatorLowLimitPercentage:37 | |
// utilizationIndicatorNormalLimitPercentage:62 | |
const studios = { | |
"1": { | |
studioName:"Brunnthal", | |
studioId:"1225817660", | |
referer:"c3BlZWRmaXRuZXNzOjEyMjU4MTc2NjA%3D", | |
tenant:"speedfitness", | |
URL:"speedfitness-brunnthal", | |
logo:"Logo_Brunnthal.png" | |
}, | |
"2": { | |
studioName:"Sendling", | |
studioId:"1217120190", | |
referer:"c3BlZWRmaXRuZXNzOjEyMTcxMjAxOTA%3D", | |
tenant:"speedfitness", | |
URL:"speedfitness-sendling", | |
logo:"Logo_Sendling.png" | |
}, | |
"3": { | |
studioName:"Bad_Aibling", | |
studioId:"1210005780", | |
referer:"c3BlZWRmaXRuZXNzOjEyMTAwMDU3ODA%3D", | |
tenant:"speedfitness", | |
URL:"speedfitness-badaibling", | |
logo:"Logo_Bad_Aibling.png" | |
}, | |
"4": { | |
studioName:"Bruckmuehl", | |
studioId:"1212577740", | |
referer:"c3BlZWRmaXRuZXNzOjEyMTI1Nzc3NDA%3D", | |
tenant:"speedfitness", | |
URL:"speedfitness-bruckmuehl", | |
logo:"Logo_Bruckmuehl.png" | |
}, | |
"5": { | |
studioName:"Grafing", | |
studioId:"1225816400", | |
referer:"c3BlZWRmaXRuZXNzOjEyMjU4MTY0MDA%3D", | |
tenant:"speedfitness", | |
URL:"speedfitness-grafing", | |
logo:"Logo_Grafing.png" | |
}, | |
"6": { | |
studioName:"Gunzenhausen", | |
studioId:"1210005450", | |
referer:"d2VoZWJhOjEyMTAwMDU0NTA%3D", | |
tenant:"speedfitness", | |
tenant:"weheba", | |
URL:"speedfitness-gunzenhausen", | |
logo:"Logo_Gunzenhausen.png" | |
}, | |
"7": { | |
studioName:"Mainburg", | |
studioId:"speedfitness-mainburg", | |
referer:"Not yet available", | |
tenant:"speedfitness", | |
URL:"speedfitness-mainburg", | |
logo:"Logo_Mainburg.png" | |
}, | |
"8": { | |
studioName:"Wasserburg", | |
studioId:"1217120110", | |
referer:"c3BlZWRmaXRuZXNzOjEyMTcxMjAxMTA%3D", | |
tenant:"speedfitness", | |
URL:"speedfitness-wasserburg", | |
logo:"Logo_Wasserburg.png" | |
} | |
} | |
console.log(studios[location].studioId) | |
const studioId = studios[location].studioId; | |
const studioURL = studios[location].URL; | |
const studioTenant = studios[location].tenant; | |
const contextSize = 282; | |
const fitXOrange = new Color("#ff8c00"); | |
const lightGrey = new Color("#bfbbbb"); | |
const widgetBackgroundColor = Color.black() | |
// Adjustments for the API CALL | |
const apiURL = "https://www.mysports.com/nox/public/v1/studios/" + studioId + "/utilization/v2/today"; | |
const reqBodyData = {}; | |
const logoURL = "https://" + studioURL + ".de/files/cto_layout/themedesigner/uploads/" + studios[location].logo; | |
const tapURL = "https://www.mysports.com/studio/" + studios[location].referer + "?ref=portal"; | |
console.log(tapURL) | |
console.log(logoURL) | |
var colorConfig = { | |
fitXOrange: new Color("#ff8c00"), | |
lightGrey: new Color("#bfbbbb"), | |
widgetBackgroundColor: Color.black(), | |
canvFillColor: new Color("#ed721b"), | |
canvStrokeColor: new Color("#B0B0B0"), | |
canvTextColor: new Color("#f7f7f7") | |
} | |
var sizeConfig = { | |
context: 282, | |
canvas: 200, | |
canvText: 45, | |
canvWidth: 10, | |
canvRadius: 90 | |
} | |
// Get the Info from Studio | |
const studioInfo = await fetchStoreInformation(); | |
// DrawContext for circle | |
// Circle Diagram vars | |
let fillColor = 'ed721b'; | |
let strokeColor = 'B0B0B0'; | |
let textColor = 'f7f7f7'; | |
const canvas = new DrawContext(); | |
const canvSize = 200; | |
const canvTextSize = 45 | |
const canvWidth = 10 | |
const canvRadius = 90; | |
canvas.opaque = false | |
canvas.size = new Size(canvSize, canvSize); | |
canvas.respectScreenScale = true; | |
// Build Widget | |
const mainWidget = await createWidget(); | |
// used for debugging if script runs inside the app | |
if (!config.runsInWidget) { | |
await mainWidget.presentSmall(); | |
} | |
Script.setWidget(mainWidget); | |
Script.complete(); | |
// ### Modules or Functions #### | |
// Build the content of the widget | |
async function createWidget() { | |
const widget = new ListWidget(); | |
widget.backgroundColor = widgetBackgroundColor; | |
//get the current date | |
var d = new Date(); | |
const currentWeekDay = d.getDay(); | |
// not sure when their time start | |
const currentHour = d.getHours(); | |
// top row this studio name and logo | |
widget.addSpacer(4); | |
const logoImg = await getImage(studios[location].logo); | |
widget.setPadding(5, 5, 5, 5); | |
const titleFontSize = 14; | |
const detailFontSize = 14; | |
// url whem tap widget | |
console.log(tapURL) | |
widget.url = tapURL; | |
// present the studio logo | |
const logoImageStack = widget.addStack(); | |
logoImageStack.backgroundColor = widgetBackgroundColor | |
logoImageStack.cornerRadius = 8; | |
const wimg = logoImageStack.addImage(logoImg); | |
wimg.centerAlignImage(); | |
if (studioInfo == "No data") { | |
wTxt = widget.addText(studioInfo) | |
wTxt.centerAlignText() | |
wTxt.textColor = new Color(textColor) | |
return widget | |
} | |
// Row1 Title Stack | |
const percentTitle = widget.addText("Live-Auslastung") | |
percentTitle.centerAlignText() | |
percentTitle.font = Font.regularSystemFont(detailFontSize); | |
percentTitle.textColor = new Color(textColor) | |
// Row2 Draw circle with percentage in the middle | |
// get the reamaining percentage | |
let remainingPercentage = (studioInfo.percentage).toFixed(0); | |
// draw canvas | |
drawArc( | |
new Point(canvSize / 2, canvSize / 2), | |
canvRadius, | |
canvWidth, | |
Math.floor(remainingPercentage * 3.6) | |
); | |
// draw text rectangle | |
const canvTextRect = new Rect( | |
0, | |
100 - canvTextSize / 2, | |
canvSize, | |
canvTextSize | |
); | |
// format the text | |
canvas.setTextAlignedCenter(); | |
canvas.setTextColor(new Color(textColor)); | |
canvas.setFont(Font.regularSystemFont(canvTextSize)); | |
canvas.drawTextInRect(`${studioInfo.percentage}%`, canvTextRect); | |
// present the image | |
const canvImage = canvas.getImage(); | |
let image = widget.addImage(canvImage); | |
image.centerAlignImage() | |
//adjust position | |
widget.addSpacer() | |
// Row3 display the time of last updastee | |
const dateLabel = widget.addDate(d); | |
dateLabel.font = Font.regularSystemFont(10); | |
dateLabel.textColor = Color.lightGray(); | |
dateLabel.applyTimeStyle(); | |
dateLabel.centerAlignText(); | |
// finished return widget | |
return widget | |
} | |
// fetches information of the configured studio | |
async function fetchStoreInformation() { | |
let url = apiURL; | |
let tenant = studioTenant; | |
if (!tenant) { | |
tenant = "speedfitness"; | |
} | |
// let data = JSON.stringify(reqBodyData); | |
let req = new Request(url); | |
req.method = "GET" | |
req.headers = { 'Connection':'keep-alive', 'Accept': 'application/json, text/plain, */*', 'referer': tapURL, 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7', 'Content-Type': 'application/json', 'x-tenant': tenant } | |
// req.body = data | |
console.log("Request:") | |
console.log(req) | |
let apiResults = await req.loadString(); | |
console.log("Response:") | |
console.log(apiResults) | |
console.log("Return Code:" + req.response.statusCode) | |
if (req.response.statusCode != 200) { | |
console.error(req.response.statusCode + "https - Bad Request") | |
return "No data" | |
} else if (req.response.statusCode == 404) { | |
// TODO: implement error handling | |
console.log("error code:" + req.response.statusCode) | |
return "No data" | |
} else if (req.response.statusCode == 200) { | |
apiResults = JSON.parse(apiResults); | |
} | |
for (let data_set of apiResults) { | |
if (data_set.current == true) { | |
apiResult = data_set | |
} | |
} | |
if (!apiResult) { | |
return "No data" | |
} | |
return apiResult; | |
} | |
// get images from local filestore or download them once | |
async function getImage(localImage) { | |
let fm = FileManager.local(); | |
let dir = fm.documentsDirectory(); | |
let path = fm.joinPath(dir, localImage); | |
if (fm.fileExists(path)) { | |
return fm.readImage(path); | |
} else { | |
// download once | |
let imageUrl = logoURL; | |
let iconImage = await loadImage(imageUrl); | |
fm.writeImage(path, iconImage); | |
return iconImage; | |
} | |
} | |
// helper function to download an image from a given url | |
async function loadImage(imgUrl) { | |
const req = new Request(imgUrl); | |
return await req.loadImage(); | |
} | |
// Helper for drawing Canvas | |
// Math Sig | |
function sinDeg(deg) { | |
return Math.sin((deg * Math.PI) / 180); | |
} | |
// Math Cos | |
function cosDeg(deg) { | |
return Math.cos((deg * Math.PI) / 180); | |
} | |
// Draw an Arc | |
function drawArc(ctr, rad, w, deg) { | |
bgx = ctr.x - rad; | |
bgy = ctr.y - rad; | |
bgd = 2 * rad; | |
bgr = new Rect(bgx, bgy, bgd, bgd); | |
canvas.setFillColor(new Color(fillColor)); | |
canvas.setStrokeColor(new Color(strokeColor)); | |
canvas.setLineWidth(w); | |
canvas.strokeEllipse(bgr); | |
for (t = 0; t < deg; t++) { | |
rect_x = ctr.x + rad * sinDeg(t) - w / 2; | |
rect_y = ctr.y - rad * cosDeg(t) - w / 2; | |
rect_r = new Rect(rect_x, rect_y, w, w); | |
canvas.fillEllipse(rect_r); | |
} | |
} | |
// Draw a line | |
function drawLine(x1, y1, x2, y2, ctx, width, color) { | |
const path = new Path(); | |
path.move(new Point(x1, y1)); | |
path.addLine(new Point(x2, y2)); | |
ctx.addPath(path); | |
ctx.setStrokeColor(color); | |
ctx.setLineWidth(width); | |
ctx.strokePath(); | |
} | |
// Draw a Point | |
function drawPoint(x, y, ctx, color, width = 2) { | |
const rec = new Rect(x - width / 2, y - width / 2, width, width); | |
ctx.setStrokeColor(color); | |
ctx.setFillColor(color); | |
ctx.setLineWidth(2); | |
ctx.strokeEllipse(rec); | |
ctx.fillEllipse(rec); | |
} | |
// Draw text | |
function drawText(text, fontSize, x, y, ctx, color = Color.black()) { | |
ctx.setFont(Font.boldSystemFont(fontSize)); | |
ctx.setTextColor(color); | |
ctx.drawText(new String(text).toString(), new Point(x, y)); | |
} |
Where do you get the STUDIO IDs from?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Intro
This script displays a widget to show the current occupancy for the Speedfitness Studios (gyms) located in Germany. It can be setup only in small size widgets. The colors are fixed to a dark appearance, because it fits better to the Speedfitness CI.
If you tap on the widget you will be forwarded to the MySports page were the data is fetched from.
It is an experimental usage an reverse-engineering of the offered Web App from MySports under https://www.mysports.com/nox/public/v1/studios/STUDIOID/* which is not an official public API but an public accessible web page.
Credits
Thanks to DanielStefanK for inspiration by offering a version for FitX Studios https://gist.github.com/DanielStefanK/487175b6f65ede401e37ee4848970176
Usage
Requirements
Installation
Configuration
You can set the parameter argument used in the code to set the location of the studio you want to display.
Following values are allowed and possible:
Updates
10.01.2023: Speedfitness is no longer supporting the old eLiveAuslastung service, switched to MySports App. Quick and dirty adoption to the new web service API.
10.11.2020: Released the initial version