Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save n0rdlicht/7ea33a3dd58220fca1dd98a128286f57 to your computer and use it in GitHub Desktop.
Save n0rdlicht/7ea33a3dd58220fca1dd98a128286f57 to your computer and use it in GitHub Desktop.
Scriptable Widget: Current Vaccination Numbers and Rates in Germany
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: green; icon-glyph: magic;
// Licence: Robert Koch-Institut (RKI), dl-de/by-2-0
// Vaccine API by @twesterhuys / https://github.com/n0rdlicht
// Location detection by @mkreuz
const options = [
"Germany",
"Baden-Württemberg",
"Bayern",
"Berlin",
"Brandenburg",
"Bremen",
"Hamburg",
"Hessen",
"Mecklenburg-Vorpommern",
"Niedersachsen",
"Nordrhein-Westfalen",
"Rheinland-Pfalz",
"Saarland",
"Sachsen",
"Sachsen-Anhalt",
"Schleswig-Holstein",
"Thüringen"
]
//Data storage
const cacheStorage = FileManager.local();
const cacheStoragePath = cacheStorage.cacheDirectory();
const pathCache = cacheStorage.joinPath(cacheStoragePath, "location.txt")
// const state = "Baden-Württemberg" // or "Germany", "Schleswig-Holstein", "Nordrhein-Westpfahlen", ...
const stateData = await getState();
const state = stateData[0];
const locationBased = stateData[1];
const validInput = stateData[2];
// Vaccination API
const vaccineStatus = encodeURI("https://api.vaccination-tracker.app/v1/de-vaccinations-current?geo=" + state);
// Initialize Widget
let widget = await createWidget();
if (!config.runsInWidget) {
// Safari.open("https://api.vaccination-tracker.app");
await widget.presentSmall();
}
Script.setWidget(widget);
Script.complete();
// Build Widget
async function createWidget(items) {
let list = new ListWidget();
// Add background gradient
let gradient = new LinearGradient();
gradient.locations = [0,1];
gradient.colors = [
new Color("141414"),
new Color("13233F")
];
list.backgroundGradient = gradient;
if(validInput){
let title = state.toUpperCase();
const number = await getVaccineData();
let quote = (Math.round(number.quote_initial*100)/100).toLocaleString();
let header, label;
list.addSpacer(3);
let titleStack = list.addStack();
header = titleStack.addText(state.toUpperCase());
header.textColor = new Color("FFFFFF");
if(state.length < 12){
header.font = Font.boldSystemFont(14);
} else {
header.font = Font.boldSystemFont(10);
}
if(locationBased){
let sym = titleStack.addImage(createSymbol("location").image);
sym.resizable = false;
sym.tintColor = Color.white();
sym.imageSize = new Size(24,16);
}
list.addSpacer(6);
let deltaVortag = "k.A.";
if(number.delta_vortag_initial > 10000){
value = Math.round(number.delta_vortag_initial / 100) / 10;
deltaVortag = "+" + value.toLocaleString() + "k";
}
else if(number.delta_vortag_initial > 1000){
value = Math.round(number.delta_vortag_initial / 10) / 100;
deltaVortag = "+" + value.toLocaleString() + "k";
}
else {
deltaVortag = "+" + number.delta_vortag_initial.toLocaleString();
}
label = list.addText(deltaVortag + "💉");
label.font = Font.mediumSystemFont(24);
label.textColor = new Color("FFFFFF");
let dF = new DateFormatter();
dF.useMediumDateStyle();
dF.useNoTimeStyle();
const since = list.addText("am " + dF.string(new Date(number.updated)));
since.font = Font.mediumSystemFont(10);
since.textColor = Color.gray();
list.addSpacer(8);
header = list.addText("Erstimpfquote".toUpperCase());
header.font = Font.mediumSystemFont(12);
header.textColor = new Color("FFFFFF");
label = list.addText(quote + "%");
label.font = Font.mediumSystemFont(24);
label.textColor = new Color("FFFFFF");
if (number.quote_initial < 30) {
label.textColor = Color.red();
} else if (number.quote_initial >= 30 && number.quote_initial < 67) {
label.textColor = Color.orange();
} else {
label.textColor = Color.green();
}
list.addSpacer(5);
let dF2 = new DateFormatter();
dF2.useShortDateStyle();
dF2.useShortTimeStyle();
const updated = list.addText("Stand: " + dF2.string(new Date(number.published)));
updated.font = Font.mediumSystemFont(10);
updated.textColor = Color.gray();
}
else {
label = list.addText("Ungültiger Parameter.");
label.font = Font.boldSystemFont(16);
label.textColor = new Color("FFFFFF");
list.addSpacer(5);
label = list.addText("Bundesland oder Germany");
label.font = Font.mediumSystemFont(14);
label.textColor = new Color("FFFFFF");
}
return list;
}
// Get vaccination rates
async function getVaccineData() {
let data = await new Request(vaccineStatus).loadJSON();
var values = {}
values["updated"] = data.last_update
values["published"] = data.last_published
data.data.forEach(row => {
if(['sum_initial','quote_initial','delta_vortag_initial'].includes(row["key"])){
if(row["value"]) {
values[row["key"]] = row["value"];
}
else {
values[row["key"]] = "k.A.";
}
}
});
return values;
}
async function getState(){
let state = "Germany";
let locationBased = false;
let valid = true;
if(args.widgetParameter && options.includes(args.widgetParameter)) {
state = args.widgetParameter;
}
else if(args.widgetParameter){
valid = false;
}
else {
const location = await getLocation();
if (location){
const lat = location.latitude;
const lng = location.longitude;
const reverse = await Location.reverseGeocode(lat, lng, "de");
const response = reverse[0];
if (response.postalAddress.state && response.postalAddress.isoCountryCode === "DE") {
state = response.postalAddress.state;
cacheStorage.writeString(pathCache, state);
locationBased = true;
}
}
else if (cacheStorage.fileExists(pathCache)) {
state = cacheStorage.readString(pathCache);
}
}
return [state, locationBased, valid];
}
async function getLocation() {
try {
if (args.widgetParameter) {
const fixedCoordinates = args.widgetParameter.split(",").map(parseFloat);
return { latitude: fixedCoordinates[0], longitude: fixedCoordinates[1] };
} else {
Location.setAccuracyToThreeKilometers();
return await Location.current();
}
} catch (e) {
return null;
}
}
function createSymbol(name) {
let font = Font.systemFont(12);
let sym = SFSymbol.named(name);
sym.applyFont(font);
return sym;
}
@n0rdlicht
Copy link
Author

Updated with some fixes for light mode and size of title for longer state names.

@n0rdlicht
Copy link
Author

New version with

  • automatic location detection
  • and configurable widget parameter to set the state or Germany
  • visual improvements

Big thanks to @mkreuz.

IMG_0560

@n0rdlicht
Copy link
Author

n0rdlicht commented Jan 8, 2021

@mkreuz we should look into caching the location, so it only runs on updates when the device is unlocked and not in the background, since location data is not available and resulting in an error.

@mkreuz
Copy link

mkreuz commented Jan 8, 2021

@n0rdlicht Good idea. I implemented a FileManager to store the last location. If no location is available, the cache is read otherwise it is updated with the current location

@n0rdlicht
Copy link
Author

@n0rdlicht Good idea. I implemented a FileManager to store the last location. If no location is available, the cache is read otherwise it is updated with the current location

Updated from the fork.

@n0rdlicht
Copy link
Author

Updated to refelect changes to RKI dataset. Now showing initial round of vaccinations for daily delta and vaccination rate.

For more detailed data and background see https://github.com/n0rdlicht/rki-vaccination-scraper

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