Skip to content

Instantly share code, notes, and snippets.

@donfelipo
Forked from DanielStefanK/fitx-widget.js
Last active June 23, 2024 17:49
Show Gist options
  • Save donfelipo/69cf704362f35c7ca5625f2e33893e24 to your computer and use it in GitHub Desktop.
Save donfelipo/69cf704362f35c7ca5625f2e33893e24 to your computer and use it in GitHub Desktop.
// 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));
}
@donfelipo
Copy link
Author

donfelipo commented Nov 10, 2020

iVBORw0KGgoAAAANSUhEUgAABS0AAAo4CAYAAAC8JoK+AAABgmlDQ1BzUkdCIElFQzYxOTY2LTIu-2

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

  • min. iOS14 device
  • iOS App "Scriptable", latest version

Installation

  • Just copy the script and insert it into a new script within Scriptable
  • Add a new Scriptable Widget on your Homescreen (Small size)
  • Configure the Widget to use the created script and choose "run script" as action

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:

  • Brunnthal => 1 ==> "default" if no parameter is present
  • Sendling => 2
  • Bad Aibling => 3
  • Bruckmühl => 4
  • Grafing => 5
  • Gunzenhausen => 6
  • Mainburg => 7 (currently not yet supported)
  • Wasserburg => 8

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

@PrintStructor
Copy link

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