Skip to content

Instantly share code, notes, and snippets.

@DanielStefanK
Last active November 3, 2023 18:57
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save DanielStefanK/487175b6f65ede401e37ee4848970176 to your computer and use it in GitHub Desktop.
Save DanielStefanK/487175b6f65ede401e37ee4848970176 to your computer and use it in GitHub Desktop.
//Can be obtained here https://fitx-proxy.daniel-stefan.dev/
let studioId = 1266927;
// Proxy for the utilization api
let proxyUrl = "https://fitx-proxy.daniel-stefan.dev/api/utilization/"
let param = args.widgetParameter;
if (param != null && param.length > 0) {
studioId = param;
}
const contextSize = 282;
const fitXOrange = new Color("#ff8c00");
const lightGrey = new Color("#bfbbbb");
const widget = new ListWidget();
let studioInfo;
try {
studioInfo = await fetchStoreInformation();
await createWidget();
} catch (e) {
console.log(e);
widget.addSpacer(4);
const logoImg = await getImage("logo.png");
widget.setPadding(10, 10, 10, 10);
const titleFontSize = 14;
const detailFontSize = 14;
const logoStack = widget.addStack();
logoStack.addSpacer(4);
const logoImageStack = logoStack.addStack();
logoStack.layoutHorizontally();
logoImageStack.backgroundColor = new Color("#ffffff", 1.0);
logoImageStack.cornerRadius = 8;
const wimg = logoImageStack.addImage(logoImg);
wimg.imageSize = new Size(40, 40);
wimg.rightAlignImage();
widget.addSpacer();
const row = widget.addStack();
row.layoutVertically();
const percentTitle = row.addText("Fehler beim Laden");
}
// used for debugging if script runs inside the app
if (!config.runsInWidget) {
await widget.presentSmall();
}
Script.setWidget(widget);
Script.complete();
// build the content of the widget
async function createWidget() {
var d = new Date();
const currentWeekDay = d.getDay();
const currentHour = Math.abs(d.getHours() - 2) % 24;
widget.addSpacer(4);
const logoImg = await getImage("logo.png");
widget.setPadding(10, 10, 10, 10);
const titleFontSize = 14;
const detailFontSize = 14;
const logoStack = widget.addStack();
const name = logoStack.addText(studioInfo.name);
name.font = Font.regularSystemFont(titleFontSize);
name.minimumScaleFactor = 0.5;
logoStack.addSpacer(4);
const logoImageStack = logoStack.addStack();
logoStack.layoutHorizontally();
logoImageStack.backgroundColor = new Color("#ffffff", 1.0);
logoImageStack.cornerRadius = 8;
const wimg = logoImageStack.addImage(logoImg);
wimg.imageSize = new Size(40, 40);
wimg.rightAlignImage();
widget.addSpacer();
const row = widget.addStack();
row.layoutVertically();
const percentTitle = row.addText("Aktuelle Auslastung: ");
percentTitle.font = Font.regularSystemFont(detailFontSize);
const percentage = row.addText(studioInfo.workload + "%");
percentage.font = Font.regularSystemFont(detailFontSize);
let drawContext = new DrawContext();
drawContext.size = new Size(contextSize, contextSize);
drawContext.opaque = false;
drawContext.setTextAlignedCenter();
const data = studioInfo.items.map((i) => i.percentage);
let min, max, diff;
for (let i = 0; i <= 24; i++) {
let temp = data[i];
min = temp < min || min == undefined ? temp : min;
max = temp > max || max == undefined ? temp : max;
}
diff = max - min;
let spaceBetweenDays = 10;
for (let i = 0; i < 24; i++) {
let current = data[i];
let nextHour = data[i + 1];
let delta = diff > 0 ? (current - min) / diff : 0.5;
let nextDelta = diff > 0 ? (nextHour - min) / diff : 0.5;
drawPoint(
spaceBetweenDays * i + 25,
175 - 50 * delta,
drawContext,
fitXOrange,
currentHour === i - 2 ? 15 : 3
);
if (i % 3 == 0) {
drawText(
data[i],
10,
spaceBetweenDays * i + (data[i] > 9 ? 16 : 22),
175 - 50 * delta - 25,
drawContext,
lightGrey
);
drawText(
i,
10,
spaceBetweenDays * i + (data[i] > 9 ? 16 : 22),
180,
drawContext,
lightGrey
);
}
drawLine(
spaceBetweenDays * i + 25,
175 - 50 * delta,
spaceBetweenDays * (i + 1) + 25,
175 - 50 * nextDelta,
drawContext,
4,
fitXOrange
);
}
widget.backgroundImage = drawContext.getImage();
}
// fetches information of the configured store, e.g. opening hours, address etc.
async function fetchStoreInformation() {
let url = proxyUrl + studioId;
let req = new Request(url);
let apiResult = await req.loadString();
if (req.response.statusCode == 404) {
// TODO: implement error handling
} else if (req.response.statusCode == 200) {
apiResult = JSON.parse(apiResult);
widget.url = "https://mein.fitx.de/studio/" + apiResult.uuid;
}
return apiResult;
}
// get images from local filestore or download them once
async function getImage(image) {
let fm = FileManager.local();
let dir = fm.documentsDirectory();
let path = fm.joinPath(dir, image);
if (fm.fileExists(path)) {
return fm.readImage(path);
} else {
// download once
let imageUrl;
switch (image) {
case "logo.png":
imageUrl = "https://i.imgur.com/2T54ySh.png";
break;
default:
console.log(`Sorry, couldn't find ${image}.`);
}
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();
}
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();
}
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);
}
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));
}
@Borussenrick
Copy link

Woher bekomme ich denn die Studio ID für mein Studio?

@DanielStefanK
Copy link
Author

@Borussenrick

  1. Offne deine Dev-Tools im Browser (f12).
  2. Offne den Netwerk-Tab.
  3. Waehle nur XHR- Request aus.
  4. Rufe die Webseite deines FitX-Studios auf.
  5. Einige Request werden mit dieser id gemacht. (z.B.: 25)
    image
  6. Uebergib diese ID als Parameter oder passe das Script

@JustMetty
Copy link

Mein Studio in Neuss hat die Studio ID 110, damit gibts leider ne Fehlermeldung. Habe die ID in Zeile 4 anstelle der 91 eingetragen. Gibts da ne Lösung?

@DanielStefanK
Copy link
Author

@JustMetty Aktuell glaube ich, dass dein Studio keine daten liefert, da es geschlossen ist. Die Anfrage, die ich benutzte wird aktuell nicht mehr auf der Webseite genutzt, da alle Studios geschlossen sind. Werde nachher mal schauen ob ich einen fix finde.

@Vecto1511
Copy link

Hey, cooles script! Gibts die möglichkeit sowas auch für Android als script zu bekommen? LG Luca

@DanielStefanK
Copy link
Author

Leider gibt es Scriptable nur für iOS.
Alternativen für Android, die die selben Features bieten, kenne ich leider auch nicht. Wenn es etwas ähnliches gibt wird aber sicherlich eine andere Sprache verwendet um das Widget zu konfigurieren. Dadurch wird der Code hier für dich unbrauchbar. Das einzige was dann für dich interessant ist die FitX API https://www.fitx.de/fitnessstudio/{studioID}/workload.

LG
Daniel

@DanielStefanK
Copy link
Author

Seems like fitx is changing its API up, I'll keep an eye on it and implement a solutione once I know how the new API works

@krmkrx
Copy link

krmkrx commented Nov 20, 2022

Thank you, most of the gyms have already switched over to the new system. Seems like access to the occupancy data is only possible once logged in to the new web application.

@DanielStefanK
Copy link
Author

For now I've build a simple proxy to retrieve the utilisation data hosted at https://fitx-proxy.daniel-stefan.dev/. This also serves a simple webpage to obtain the new studio ID for the script.

The proxy implementation can be found here.
I think the data can also be retrieved from the fitx API in the script itself, because there are public endpoints for the both the studios (https://mein.fitx.de/sponsorship/v1/public/studios/forwhitelabelportal) and utilisation (https://mein.fitx.de/nox/public/v1/studios/{magiclineid}/utilization), but I've build this proxy with the thought that it could only be obtained with an authenticated user.
At this point i am not sure if the magicline id changes over time for each studio.
Also as far as I can see the API does not predict the future utilisation so the graph is flat from the current time.
I will keep an eye on the API and will update the script/shut down the proxy when there is a better way to get the data.
For now this works for the current/past utilisation and is sufficient for me.

@krmkrx
Copy link

krmkrx commented Nov 23, 2022

This works flawlessly so far, many thanks! Wouldn’t be possible to provide for an implementation to be used in connection with this simplified widget style created by someone else?

https://gist.github.com/masselmello/6d4f4c533b98b2550ee23a7a5e6c6cff?permalink_comment_id=3500390#gistcomment-3500390

The respective (now non-working) FitX fork can be found here: https://gist.github.com/eopo/aedaf03f1f27a0c9c02f4974f3f4c9ba

It would be great to somehow have a simplified widget without past data, as shown in the example above. 🙂

@DanielStefanK
Copy link
Author

My proxy should work with the other widget. The current workload is found on the response.
I've updated the gist with the current api here: https://gist.github.com/DanielStefanK/adf0fc40d08585e4167f20463fe3b1d1

@krmkrx
Copy link

krmkrx commented Nov 23, 2022

Thank you very much for this, I appreciate this as I am not super knowledgeable with regard to GitHub and Scriptable in general.

@brusayannick
Copy link

Seeehr geiles script! Habe ich zwar nicht nach gesucht aber installiert. Danke dir <3

@9dc
Copy link

9dc commented May 8, 2023

finds geil. location service an sich wäre noch nice. ist mit scriptable ja problemlos möglich. Also immer das FitX anzeigen, was in der nähe ist. es müssten nur die locations in der proxy gespeichert werden..

@HeyMeco
Copy link

HeyMeco commented Nov 3, 2023

Habe aktuell das Problem das der Graph nicht mehr gerendert wird. Nur ich?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment