Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
iOS Widget, welches anzeigt ob im lokalen Impfzentrum Vermittlungscodes verfügbar sind. (für die scriptable.app)
/*
Impftermin Widget
v 1.3.1 EOL of project
This Scriptable Widget will show you if there are any "Vermittlungscode" for vaccination appointments available.
The data is pulled from the impfterminservice.de api, which is neither publicly available nor documented.
Therefore everything may break.
The newest version, issues, etc. of this widget can be found here: https://github.com/not-a-feature/impfWidget
A gist version is also available: https://gist.github.com/not-a-feature/4e6dbbd9eb3bd927e50cae347b7e0486/
The framework/skeleton of this script was created by marco79cgn for the toiletpaper-widget
(https://gist.github.com/marco79cgn/23ce08fd8711ee893a3be12d4543f2d2)
To uses this widget go to https://003-iz.impfterminservice.de/assets/static/impfzentren.json and search for
your local center. Copy the whole text in between the two curly brackets and paste it below in the settings (Starting at line 55).
If you want a notification change the NOTIFICATION_LEVEL to
0: no notification
1: only if vaccines are available
2: every time the widget refreshes
If you want to know if there are appointments specifically for a vaccine,
set DISPLAY_VACCINES_AS_ONE to false. This requires a medium size-widget (2x1)
If you want to exclude specific vaccines, set them to inside the VACCINES variable to false.
Thats it. You can now run this script.
Copy the source, open the scriptabel app, add the source there.
go the home screen, add scriptable widget
-------------------------------------------------------------------------------
LICENSE:
Copyright (C) 2021 by Jules Kreuer - @not_a_feature
This piece of software is published unter the GNU General Public License v3.0
TLDR:
| Permissions | Conditions | Limitations |
| ---------------- | ---------------------------- | ----------- |
| ✓ Commercial use | Disclose source | ✕ Liability |
| ✓ Distribution | License and copyright notice | ✕ Warranty |
| ✓ Modification | Same license | |
| ✓ Patent use | State changes | |
| ✓ Private use | | |
Go to https://github.com/not-a-feature/impfWidget/blob/main/LICENSE to see the full version.
------------------------------------------------------------------------------- */
//-----------------------------------------------------------------------------
// Settings
// Replace this with the data of you local center
const CENTER = {
"Zentrumsname": "Paul Horn Arena",
"PLZ": "72072",
"Ort": "Tübingen",
"Bundesland": "Baden-Württemberg",
"URL": "https://003-iz.impfterminservice.de/",
"Adresse": "Europastraße 50"
}
// adjust to your desired level
const NOTIFICATION_LEVEL = 1
// Set to false, if a detailed view is wanted.
// Attention! This requires a medium size-widget (2x1)
const DISPLAY_VACCINES_AS_ONE = true
// Advanced Setting
// Fetch status of following vaccines, set to false to ignore this vaccine
const VACCINES = [{"name": "BioNTech", "ID": "L920", "allowed": true},
{"name": "Moderna", "ID": "L921", "allowed": true},
{"name": "AstraZeneca", "ID": "L922", "allowed": true}]
// END Setting
//-----------------------------------------------------------------------------
const vaccineTextFontSize = 13
const appointmentsTextFontSize = 22
const detailTextFontSize = 17
const textColorRed = new Color("#E50000")
const textColorGreen = new Color("#00CD66")
const widget = new ListWidget()
widget.url = CENTER["URL"]
const openAppointments = await fetchOpenAppointments()
await createNotification()
await createWidget()
if (!config.runsInWidget) {
if (DISPLAY_VACCINES_AS_ONE) {
await widget.presentSmall()
}
else {
await widget.presentMedium()
}
}
Script.setWidget(widget)
Script.complete()
/* create Widget
case: smallWidget (DISPLAY_VACCINES_AS_ONE == true)
topRow: | leftColumn | rightColumn |
| | IMPFUNGEN |
| icon | Keine/Termine |
bottomRow: | Location |
case: mediumWidget (DISPLAY_VACCINES_AS_ONE == false)
topRow: | leftColumn | rightColumn | detailColumn |
| | IMPFUNGEN | BioNTech |
| icon | Keine/Termine | Moderna... |
bottomRow: | Location |
*/
/*
Create widget using current information
*/
async function createWidget() {
widget.setPadding(10, 10, 10, 10)
const icon = await getImage('vaccine')
let topRow = widget.addStack()
topRow.layoutHorizontally()
let leftColumn = topRow.addStack()
leftColumn.layoutVertically()
leftColumn.addSpacer(vaccineTextFontSize)
const iconImg = leftColumn.addImage(icon)
iconImg.imageSize = new Size(40, 40)
topRow.addSpacer(vaccineTextFontSize)
let rightColumn = topRow.addStack()
rightColumn.layoutVertically()
const vaccineText = rightColumn.addText("IMPFUNGEN")
vaccineText.font = Font.mediumRoundedSystemFont(vaccineTextFontSize)
let openAppointmentsText
let textColor = textColorRed
if (openAppointments.hasOwnProperty("error")) {
openAppointmentsText = "⚠️ " + openAppointments["error"]
}
else if (Object.values(openAppointments).includes(true)) {
openAppointmentsText = "Freie\nTermine"
textColor = textColorGreen
}
else {
openAppointmentsText = "Keine\nTermine"
}
let openAppointmentsTextObj = rightColumn.addText(openAppointmentsText)
openAppointmentsTextObj.font = Font.mediumRoundedSystemFont(appointmentsTextFontSize)
openAppointmentsTextObj.textColor = textColor
if(!DISPLAY_VACCINES_AS_ONE) {
topRow.addSpacer(8)
let detailColumn = topRow.addStack()
detailColumn.layoutVertically()
openAppointmentsDetail = {}
Object.keys(openAppointments).forEach((key, index) => {
openAppointmentsDetail[key] = detailColumn.addText(key)
openAppointmentsDetail[key].font = Font.mediumRoundedSystemFont(detailTextFontSize)
if (openAppointments[key]) {
openAppointmentsDetail[key].textColor = textColorGreen
}
else {
openAppointmentsDetail[key].textColor = textColorRed
}
})
}
widget.addSpacer(4)
const bottomRow = widget.addStack()
bottomRow.layoutVertically()
// Replacing long names with their abbrehivations
let shortName = CENTER["Zentrumsname"]
shortName = shortName.replace("Zentrales Impfzentrum", "ZIZ")
shortName = shortName.replace("Zentrales Impfzentrum (ZIZ)", "ZIZ")
shortName = shortName.replace("Landkreis", "LK")
shortName = shortName.replace("Kreisimpfzentrum", "KIZ")
shortName = shortName.replace("Impfzentrum Kreis", "KIZ")
shortName = shortName.replace("Impfzentrum Landkreis", "KIZ")
const street = bottomRow.addText(shortName)
street.font = Font.regularSystemFont(11)
const zipCity = bottomRow.addText(CENTER["Adresse"] + ", " + CENTER["Ort"])
zipCity.font = Font.regularSystemFont(11)
}
/*
Create notification if turned on
*/
async function createNotification() {
if (NOTIFICATION_LEVEL > 0) {
const notify = new Notification();
notify.sound = "default";
notify.title = "ImpfWidget";
notify.openURL = CENTER["URL"];
if (Object.values(openAppointments).includes(true)) {
notify.body = "💉 Freie Termine"
notify.schedule();
return;
}
else if (openAppointments.hasOwnProperty("error")) {
notify.body = "⚠️ " + openAppointments["error"]
notify.schedule();
return;
}
else if (NOTIFICATION_LEVEL == 2) {
notify.body = "🦠 Keine Termine"
notify.schedule();
return;
}
}
}
/*
Fetches open appointments
Returns object e.g:
{"BioNTech": true, "Monderna": false}
or {"Error": "Error message"}
*/
async function fetchOpenAppointments() {
let url = CENTER["URL"] + "rest/suche/termincheck?plz=" + CENTER["PLZ"] + "&leistungsmerkmale="
let result = {}
console.log(VACCINES)
// Case if all vaccines are displayed as one
if (DISPLAY_VACCINES_AS_ONE) {
let urlAppendix = []
for (var i = 0; i < VACCINES.length; i++) {
if (VACCINES[i]["allowed"]) {
urlAppendix.push(VACCINES[i]["ID"])
}
}
if (urlAppendix == []) {
return {"error": "No vaccines selected."}
}
url = url + urlAppendix.join(",")
let req = new Request(url)
let body = await req.loadString()
for (var i = 0; i < VACCINES.length; i++) {
if (body == '{"termineVorhanden":false}') {
result[VACCINES[i]["name"]] = false
}
else if (body == '{"termineVorhanden":true}') {
result[VACCINES[i]["name"]] = true
}
else if (body == '{"error":"Postleitzahl ungueltig"}') {
return {"error": "Wrong PLZ"}
}
else {
return {"error": "Error"}
}
}
}
// Case if all vaccines are displayed one by one
else {
for (var i = 0; i < VACCINES.length; i++) {
if (VACCINES[i]["allowed"]) {
console.log("Checking Vaccine: " + VACCINES[i]["name"])
let req = new Request(url + VACCINES[i]["ID"])
let body = await req.loadString()
if (body == '{"termineVorhanden":false}') {
result[VACCINES[i]["name"]] = false
}
else if (body == '{"termineVorhanden":true}') {
result[VACCINES[i]["name"]] = true
}
else if (body == '{"error":"Postleitzahl ungueltig"}') {
return {"error": "Wrong PLZ"}
}
else {
return {"error": "Error"}
}
}
}
}
console.log(result)
return result
}
// 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, save in local storage
let imageUrl
switch (image) {
case 'vaccine':
imageUrl = "https://api.juleskreuer.eu/syringe-solid.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()
}
@not-a-feature

This comment has been minimized.

Copy link
Owner Author

@not-a-feature not-a-feature commented Feb 9, 2021

impfWidget

EOL des Projektes

Disclaimer

Es handelt sich um ein von mir selbst entwickeltes Spaßprojekt, es ist weder ein offizielles Produkt noch steht es im Zusammenhang mit der 116/117 oder impfterminservice.de.

Installation & Git-Repo

Eine Anleitung, Pullrequests, Issues und Kommentare finden auf dem Git Repo: https://github.com/not-a-feature/impfWidget
Auf dem GIST (hier) wird nur die neuste Version aktualisiert.

Changelog

  • v 1.3.1 EOL of project
  • v 1.3.0 introducing Notifications
  • v 1.2.3 changing api to subdomain
  • v 1.2.2 changing licence url & comment
  • v 1.2.1 removing whitespace in name-replace function
  • v 1.2 added detail-view and updated licence to GNU GPLv3
  • v 1.1.1 fixed typo in BLAND ID explanation
  • v 1.1 added AstraZeneca Vaccine (L922)
  • v. 1.0 initial Release
@cytrex-de

This comment has been minimized.

Copy link

@cytrex-de cytrex-de commented Mar 4, 2021

Vielen Dank! Funktioniert super und war genau was ich gesucht hab.

Wollte erst selbst den JSON mit JS auslesen und anzeigen, aber bekomme ständig CORS Fehler. Wieso funktioniert das bei dir? (sorry für die dumme Frage :/)

@cytrex-de

This comment has been minimized.

Copy link

@cytrex-de cytrex-de commented Mar 4, 2021

Für Ersttermine muss es anscheinend ein Terminpaar geben, damit freie Termine angezeigt werden.

Bsp. für ein Impfzentrum wird folgendes zurück geliefert: {"termineVorhanden":true} Wenn man dann einen Termin buchen möchte gibt es keine Termine, der Json von terminpaare liefert folgendes:

{
"gesuchteLeistungsmerkmale": [
"L922"
],
"terminpaare": [],
"praxen": {}
}

@not-a-feature

This comment has been minimized.

Copy link
Owner Author

@not-a-feature not-a-feature commented Mar 4, 2021

Dadurch dass Scriptable kein Browser ist und CORS / SOP nicht implementiert hat brauche ich mich damit nicht zu sorgen.
Browser hingegen erlauben es nicht, Inhalte von anderen Quellen (also Cross Origin) nachzuladen wenn keine passende CORS-Header in der Antwort gesetzt sind. Das zu umgehen ist i.d.R nicht ohne weiteres Möglich.

@not-a-feature

This comment has been minimized.

Copy link
Owner Author

@not-a-feature not-a-feature commented Mar 4, 2021

Zum Zweiten Komentar:
Ja das ist mir bewusst, dieses Widget ist (aktuell) nur zur Vermittlungscode-Vergabe (steht auch so in der Anleitung/Code).

Gestern hatte ich eine gute Konversation diesbezüglich: https://twitter.com/not_a_feature/status/1367152386909888515?s=21
Gerne kannst du auf dem Git Repo aber ein Issue aufmachen, ich habe schon eine Lösung dafür im Kopf: https://github.com/not-a-feature/impfWidget/issues

@mherbrich

This comment has been minimized.

Copy link

@mherbrich mherbrich commented Apr 20, 2021

Funktioniert das Script nicht mehr? Bei mir kommt immer Error. Bekomme auch einen leeren Body, wenn ich die Rest URL per Postman aufrufe. Vermutlich weil kein Cookie im Header gesetzt ist.

@not-a-feature

This comment has been minimized.

Copy link
Owner Author

@not-a-feature not-a-feature commented Apr 21, 2021

Siehe: not-a-feature/impfWidget#6
EOL des Projektes, da aktiv gegen crawling gearbeitet wurde

@mherbrich

This comment has been minimized.

Copy link

@mherbrich mherbrich commented Apr 22, 2021

Siehe: not-a-feature/impfWidget#6
EOL des Projektes, da aktiv gegen crawling gearbeitet wurde

Schade, trotzdem danke für deine Mühe!

@andgolearning

This comment has been minimized.

Copy link

@andgolearning andgolearning commented May 1, 2021

Hi, schade das so ein tool nicht offiziell ist. Stattdessen können wir eine lame Hotline anrufen (wenn man durchkommt) oder die Homepage 11... aufrufen und sich an der Wartezeit bis das man überhaupt zur Abfrage gelangt erfreuen. Schade, schade. Das wäre ein schöner Schritt Richtung Digitalisierung gewesen.

@Illum2021

This comment has been minimized.

Copy link

@Illum2021 Illum2021 commented May 13, 2021

Hallo,
ist es möglich das widget so umzuschreiben, dass die unterschiedlichen Vakzine in einem KIZ angezeigt werden? Einen Vermittlungscode habe ich ja, und manchmal auch einen Termin, aber halt immer mit Johnson&Johnson

@not-a-feature

This comment has been minimized.

Copy link
Owner Author

@not-a-feature not-a-feature commented May 15, 2021

Die Funktion wir für die Vermittlungscodes schon unterstützt. Da aber die eigentlichen Termine nicht (ohne weiteres) gecrawled werden können ist das mit einem hohen Mehraufwand verbunden.

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