Skip to content

Instantly share code, notes, and snippets.

@marco79cgn
Last active October 6, 2025 12:04
Show Gist options
  • Select an option

  • Save marco79cgn/b13719df059d1e8d3277af8216a4d340 to your computer and use it in GitHub Desktop.

Select an option

Save marco79cgn/b13719df059d1e8d3277af8216a4d340 to your computer and use it in GitHub Desktop.
iOS Widget, das die Anzahl an Klopapier und Mehl Packungen in deiner nächsten dm Drogerie anzeigt (für die scriptable.app)
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: gray; icon-glyph: magic;
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: green; icon-glyph: magic;
// dm Klopapier & Mehl Widget
//
// Copyright (C) 2020 by marco79 <marco79cgn@gmail.com>
//
// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
// OF THIS SOFTWARE.
//
// Wheat flour icon made by Freepik from www.flaticon.com and modified by achisto
// Toilet paper icon made by boettges
let country = 'de' // replace with 'at' for shops in Austria
let storeId = "D0CC"
let param = args.widgetParameter
if (param != null && param.length > 0) {
storeId = param
}
const widget = new ListWidget()//
const storeInfo = await fetchStoreInformation()
const storeCapacityPaper = await fetchAmountOfPaper()
const storeCapacityFlour = await fetchAmountOfWheatFlour()
await createWidget()
// 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() {
const logoImg = await getImage('dm-logo.png')
widget.setPadding(6,6,6,6)
const titleFontSize = 12
const detailFontSize = 36
const logoStack = widget.addStack()
const shopStateStack = logoStack.addStack()
shopStateStack.layoutVertically()
shopStateStack.addSpacer(10)
let currentTime = new Date().toLocaleTimeString('de-DE', { hour: "numeric", minute: "numeric" })
let currentDay = new Date().getDay()
let isOpen
if (currentDay > 0) {
const todaysOpeningHour = storeInfo.openingHours[currentDay-1].timeRanges[0].opening
const todaysClosingHour = storeInfo.openingHours[currentDay-1].timeRanges[0].closing
const range = [todaysOpeningHour, todaysClosingHour];
isOpen = isInRange(currentTime, range)
} else {
isOpen = false
}
let shopStateText
if (isOpen) {
shopStateText = shopStateStack.addText('GEÖFFNET')
shopStateText.textColor = new Color("#00CD66")
logoStack.addSpacer(36)
} else {
shopStateText = shopStateStack.addText('GESCHLOSSEN')
shopStateText.textColor = new Color("#E50000")
logoStack.addSpacer(10)
}
shopStateText.font = Font.boldRoundedSystemFont(12)
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(32,32)
wimg.rightAlignImage()
widget.addSpacer(6)
// toilet paper
const toiletPaperIcon = await getImage('toilet-paper.png')
let row = widget.addStack()
row.layoutHorizontally()
row.addSpacer(2)
const toiletPaperImg = row.addImage(toiletPaperIcon)
toiletPaperImg.imageSize = new Size(30,30)
row.addSpacer(13)
let column = row.addStack()
column.layoutVertically()
const paperText = column.addText("KLOPAPIER")
paperText.font = Font.mediumRoundedSystemFont(11)
const packageCount = column.addText(storeCapacityPaper.toString())
packageCount.font = Font.mediumRoundedSystemFont(18)
if (storeCapacityPaper < 30) {
packageCount.textColor = new Color("#E50000")
} else {
packageCount.textColor = new Color("#00CD66")
}
widget.addSpacer(4)
// wheat flour
const flourIcon = await getImage('wheat-flour.png')
let row2= widget.addStack()
row2.layoutHorizontally()
row2.addSpacer(3)
const flourImg = row2.addImage(flourIcon)
flourImg.imageSize = new Size(30,30)
row2.addSpacer(12)
let column2 = row2.addStack()
column2.layoutVertically()
const flourText = column2.addText("MEHL")
flourText.font = Font.mediumRoundedSystemFont(11)
const flourPackageCount = column2.addText(storeCapacityFlour.toString())
flourPackageCount.font = Font.mediumRoundedSystemFont(18)
if (storeCapacityFlour < 30) {
flourPackageCount.textColor = new Color("#E50000")
} else {
flourPackageCount.textColor = new Color("#00CD66")
}
widget.addSpacer(4)
// shop info
const row3 = widget.addStack()
row3.layoutVertically()
const street = row3.addText(storeInfo.address.street)
street.font = Font.regularSystemFont(10)
const zipCity = row3.addText(storeInfo.address.zip + " " + storeInfo.address.city)
zipCity.font = Font.regularSystemFont(10)
}
// fetches the amount of toilet paper packages
async function fetchAmountOfPaper() {
let url
let counter = 0
if (country.toLowerCase() === 'at') {
// Austria
const array = ["156754", "180487", "194066", "188494", "194144", "273259", "170237", "232201", "170425", "283216", "205873", "205874", "249881", "184204"]
for (var i = 0; i < array.length; i++) {
let currentItem = array[i]
url = 'https://products.dm.de/store-availability/AT/products/dans/' + currentItem + '/stocklevel?storeNumbers=' + storeId
let req = new Request(url)
let apiResult = await req.loadJSON()
if (req.response.statusCode == 200) {
counter += apiResult.storeAvailability[0].stockLevel
}
}
} else {
// Germany
url = 'https://products.dm.de/availability/api/v1/tiles/DE/1587820,1674098,1673782,1673958,1700999,1620539,1319294,1650106,1612687,1626481,1673728,2477838,1618786,1617838,1126888,1533020,1617853,2477892,2477883,1625271,1692239,1616535,1533284,1533061?pickupStoreId=' + storeId
const req = new Request(url)
const apiResult = await req.loadJSON()
let totalAmount = parseInt('0', 10)
for (const key in apiResult) {
if (apiResult[key].a11yLabel) {
const matches = apiResult[key].a11yLabel.match(/\((\d+)\)/g);
if (matches) {
matches.forEach(match => {
totalAmount += parseInt(match.replace(/[()]/g, ""), 10);
});
}
}
}
counter = totalAmount
}
return counter
}
// fetches the amount of wheat flour packages
async function fetchAmountOfWheatFlour() {
let url
let counter = 0
if (country.toLowerCase() === 'at') {
// Austria
const array = ["178501", "178491", "178498", "178484", "171412", "178489", "295198"]
for (var i = 0; i < array.length; i++) {
let currentItem = array[i]
url = 'https://products.dm.de/store-availability/AT/products/dans/' + currentItem + '/stocklevel?storeNumbers=' + storeId
let req = new Request(url)
let apiResult = await req.loadJSON()
if (req.response.statusCode == 200) {
counter += apiResult.storeAvailability[0].stockLevel
}
}
} else {
// Germany
url = 'https://products.dm.de/availability/api/v1/tiles/DE/1446464,1639885,1446521,1440266,1438255,1575844,1445541,1446462,1446463?pickupStoreId=' + storeId
const req = new Request(url)
const apiResult = await req.loadJSON()
let totalAmount = parseInt('0', 10)
for (const key in apiResult) {
if (apiResult[key].a11yLabel) {
const matches = apiResult[key].a11yLabel.match(/\((\d+)\)/g);
if (matches) {
matches.forEach(match => {
totalAmount += parseInt(match.replace(/[()]/g, ""), 10);
});
}
}
}
counter = totalAmount
}
return counter
}
// fetches information of the configured store, e.g. opening hours, address etc.
async function fetchStoreInformation() {
let url
if (country.toLowerCase() === 'at') {
url = 'https://store-data-service.services.dmtech.com/stores/item/at/' + storeId
widget.url = 'https://www.dm.at/search?query=toilettenpapier&searchType=product'
} else {
url = 'https://store-data-service.services.dmtech.com/stores/item/' + storeId
widget.url = 'https://www.dm.de/search?query=toilettenpapier&searchType=product'
}
let req = new Request(url)
let apiResult = await req.loadJSON()
return apiResult
}
// checks whether the store is currently open or closed
function isInRange(value, range) {
return value >= range[0] && value <= range[1];
}
// 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 'dm-logo.png':
imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Dm_Logo.svg/300px-Dm_Logo.svg.png"
break
case 'toilet-paper.png':
imageUrl = "https://i.imgur.com/Uv1qZGV.png"
break
case 'wheat-flour.png':
imageUrl = "https://i.imgur.com/gwWtMWn.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()
}
// End of script
// Bitte bis zum Ende alles markieren und kopieren
@SchneHa
Copy link

SchneHa commented Jan 30, 2024

Das Widget funktioniert nicht mehr, da die Daten das falsche Format haben. DM scheint was geändert zu haben.

@SchneHa
Copy link

SchneHa commented Jan 31, 2024

Jetzt geht es zwar wieder, zeigt aber bei Klopapier und Mehl jeweils „0“ an.

@marco79cgn
Copy link
Author

In den Zeilen 161 und 189 sind jeweils die ganzen Produktnummern für Klopapier und Mehl. Die haben aber einen Stand von Oktober 2020. Man müsste sich mal die Mühe machen und alle aktuellen Produktnummern zusammen suchen.

@SchneHa
Copy link

SchneHa commented Jan 31, 2024

ok, ich habe mal ein paar aktuelle Produktnummern rausgesucht und eingesetzt. Es wird aber immer noch "0" angezeigt.

@SchneHa
Copy link

SchneHa commented Jan 31, 2024

Allerdings scheint auch die Store ID eine andere zu sein, obwohl die Adresse richtig angezeigt wird. Statt 2654 steht in der URL jetzt d4ee und das kann das Script nicht handlen.

@SchneHa
Copy link

SchneHa commented Jan 31, 2024

Wer es einmal ausprobieren möchte, die Artikelnummern in der Zeile 161 sind:
975799,877955,949772,799358,952683,952170,951954,870886,863567,985619,535981,879536,856834,853483,919184,127048,836029,777180,836029,871279,593761,137425,593802,876541
und in Zeile 189:
459912,755784,468178,468120,468168,721676,531500,849142

Rausgesucht auf der DM Seite, geht aber leider nicht, Summe ist 0.

@marco79cgn
Copy link
Author

Ich habe gerade eben das Skript oben angepasst, für Deutschland funktioniert es wieder. Österreich muss ich mir noch anschauen, vor allem was die Produktnummern angeht.

Die API von dm hat sich geändert bzw. die Endpunkte. Sowohl die Store Informationen als auch die Verfügbarkeits-URLs. Zudem haben sich offenbar alle Store IDs geändert. Bitte neu ermitteln auf dieser Seite.

@SchneHa
Copy link

SchneHa commented Mar 1, 2025

Das Widget geht seit einigen Tagen nicht mehr: „Die Daten konnten nicht gelesen werden, da sie nicht das korrekte Format haben.“ DM scheint irgend etwas geändert zu haben, was muss jetzt im Script geändert werden?

@SchneHa
Copy link

SchneHa commented Mar 6, 2025

Noch keine Lösung in Sicht?

@SchneHa
Copy link

SchneHa commented Mar 7, 2025

Mit folgenden Artikelnummern funktioniert das Widget wieder (für Deutschland, Österreich habe ich nicht gesucht). Wahrscheinlich hat DM eine oder mehrere Artikelnummer(n) raus genommen weswegen es nicht mehr ging (vermutlich beim Mehl).

Klopapier:
1587820,1674098,1673782,1673958,1700999,1620539,1319294,1650106,1612687,1626481,1673728,2477838,1618786,1617838,1126888,1533020,1617853,2477892,2477883,1625271,1692239,1616535,1533284,1533061

Mehl:
1446464,1639885,1446521,1440266,1438255,1575844,1445541,1446462,1446463

@SchneHa
Copy link

SchneHa commented Sep 9, 2025

Marco, kannst du bitte noch mal sehen woran es liegt, dass seit ein paar Wochen bei beiden Artikelgruppen nur noch „0“ angezeigt wird. Ich finde die Ursache nicht. An den Artikelnummern liegt es scheinbar nicht.

@SchneHa
Copy link

SchneHa commented Sep 15, 2025

Noch keine Lösung in Sicht?

@marco79cgn
Copy link
Author

Bin aktuell im Urlaub. Schau es mir zeitnah an.

@SchneHa
Copy link

SchneHa commented Oct 2, 2025 via email

@marco79cgn
Copy link
Author

Die haben das API komplett geändert. Bin an einer Lösung dran.

@marco79cgn
Copy link
Author

Update:
Deutschland funktioniert wieder. Bitte Skript neu kopieren. Für Östererich fehlen mir aktuelle Produktnummern (dan). Falls die jemand nachreichen könnte, korrigiere ich das auch.

@SchneHa
Copy link

SchneHa commented Oct 6, 2025

Danke Marco. Habe alles kopiert und eingesetzt. Bei mir wird allerdings immer noch „0 0“ angezeigt.

@marco79cgn
Copy link
Author

Sorry, war spät gestern. Habe die falsche Version rein kopiert. Probier jetzt nochmal.

@SchneHa
Copy link

SchneHa commented Oct 6, 2025

Wunderbar, funktioniert! Danke

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