Die Skript-Schnittstelle
Skripte sind Python. TrakState führt sie in einem eigenen Prozess aus — ohne Datenbank, ohne Zugriff auf das Dateisystem der Anlage, ins Netz nur so weit, wie Ihre Installation es erlaubt. Alles, was hinein- und hinausgeht, geht über die wenigen Namen auf dieser Seite — Tags, die Uhr, der Assistent und die Dateiablage Ihrer Installation.
Ein Wort, unter dem alles steht
Abschnitt betitelt „Ein Wort, unter dem alles steht“Alles, was TrakState anbietet, hängt unter system.. Tippen Sie system. und den Punkt,
dann zeigt Ihnen der Editor den Rest. Sie müssen keinen einzigen Namen kennen, um zu erfahren,
welche es gibt — Sie müssen ein Wort kennen.
wert = system.tag.read("Press1.Cycle")system.tag.write("Press1.Setpoint", 42)Die Gliederung sagt selbst etwas: system.tag.* ist die Tag-Schicht, system.time.* die Uhr.
Unter system. steht nur, was von uns kommt. round(), json, datetime, str sind
Python und bleiben Python — ein system.util.round gibt es nicht und wird es nicht geben. Was
in jedem Python-Handbuch steht, wickeln wir nicht ein; sonst wäre system. eine Behauptung
darüber, was uns gehört, und Sie suchten unsere Hilfe nach etwas ab, das dort nie stehen wird.
Blank bleibt, was der Vertrag Ihres Skripts ist: inputs, params, result, on_read,
on_write. Das ist keine Bibliothek, die Sie aufrufen, sondern die Form, in der Ihr Skript
sich anbietet. system.result wäre kein besserer Name für dieselbe Sache, sondern eine falsche
Aussage darüber, was sie ist.
Ein Vertipper endet deshalb nicht in einem Rätsel, sondern in einer Antwort auf die Frage, die Sie tatsächlich haben — was es denn stattdessen gibt:
AttributeError: no 'raed' here - this namespace has: read, writeDie früheren Namen
read_tagundwrite_taggibt es nicht mehr, ersatzlos und ohne Übergangsfrist. Ein Skript, das sie noch aufruft, scheitert mit einemNameError. Ersetzen Sie sie durchsystem.tag.read()undsystem.tag.write().
Zwei Bauformen
Abschnitt betitelt „Zwei Bauformen“Welche Form Sie haben, entscheidet TrakState daran, ob Sie on_read oder on_write
schreiben. Sie stellen das nirgends ein.
Einfach
Abschnitt betitelt „Einfach“Kein def on_read. Ihr Code ist der Rumpf einer Funktion — return funktioniert also,
und wer nichts zurückgibt, dessen Variable result ist das Ergebnis.
# Der Rueckgabewert ist der Wert des Tags.return inputs["good"] + inputs["scrap"]# Gleichwertig, ohne return:result = inputs["good"] + inputs["scrap"]Ein Tag in dieser Form ist nur lesbar. Ein Schreibversuch darauf wird abgelehnt, nicht still verworfen.
Mit Haken
Abschnitt betitelt „Mit Haken“Sie schreiben def on_read() und/oder def on_write(value). Der übrige Code steht dann auf
Modulebene und läuft bei jedem Aufruf, bevor der Haken drankommt — das ist das normale
Verhalten eines Python-Moduls.
INCH = 25.4
def on_read(): return inputs["length_mm"] / INCH
def on_write(value): # Was der Setter zurueckgibt, wird der eigene Wert dieses Tags. system.tag.write("Press1.Length_mm", value * INCH) return valueEin Tag mit on_write ist schreibbar, eines ohne nicht. Ein Skript, das nur on_write
hat, liefert beim Lesen nichts — es hat gesagt, was es tut, und Lesen war nicht dabei.
Die Haken heißen
on_read/on_writeund nichtget/set, weilsetein Python-Builtin ist. Wer es überschreibt, macht ein späteresset([1, 2])im selben Skript kaputt.
Was zur Verfügung steht
Abschnitt betitelt „Was zur Verfügung steht“| Name | Aufruf | Bedeutung |
|---|---|---|
system.tag.read |
system.tag.read(path) |
Liest ein Tag sofort, nach seinem Pfad. Scheitert laut, statt None zu liefern — ein still fehlender Wert wird weiter unten zu einer falschen Zahl. |
system.tag.write |
system.tag.write(path, value) |
Beantragt eine Schreibung. Aus dem Skript geht nichts hinaus: der Wirt führt die Schreibungen danach aus — oder eben nicht, und genau das ist ein Probelauf. |
system.time.plant_now |
system.time.plant_now() |
Die Zeit auf der Uhr, nach der die Anlage läuft, mit ihrem Zeitversatz. Nicht dasselbe wie datetime.now(), das die Zone der ausführenden Maschine ist. |
system.ai.ask |
system.ai.ask(prompt, data=None) |
Fragt das Sprachmodell und liefert seine Antwort als Text. Die Zahlen gehören in data — das Modell formuliert, erklärt und schlägt vor, es rechnet nicht. Nur in Skript-Objekten; jede Frage zählt gegen das KI-Budget. |
system.file.list |
system.file.list(folder, pattern=None) |
Die Dateien eines Ordners der Dateiablage (nicht seiner Unterordner), jede als Wörterbuch mit id, folder, name, size, media_type, sha256, source, created_at. pattern im Shell-Stil ("*.zip"), Groß- und Kleinschreibung zählt nicht. Nur in Skript-Objekten; jeder Ordner außer Chat/. |
system.file.read |
system.file.read(file_id) |
Kopiert eine abgelegte Datei in diesen Lauf und liefert den lokalen Pfad — öffnen Sie ihn wie jede Datei. Je Lauf in Bytes und Dateien begrenzt; die Kopie ist mit dem Lauf weg. |
system.file.write |
system.file.write(folder, name, bytes_or_path) |
Legt eine Datei ab: Bytes, oder der Pfad einer Datei, die das Skript geschrieben hat. Text braucht vorher .encode("utf-8") — ein str wird als Pfad gelesen. Im Probelauf wird nichts abgelegt. |
system.file.move |
system.file.move(file_id, folder) |
Verschiebt eine abgelegte Datei in einen anderen Ordner; ihre ID bleibt. Im Probelauf wird nichts verschoben. |
system.file.claim |
system.file.claim(file_id, key="default") |
Nimmt eine Datei für diesen Lauf, atomar: True — einlesen; False — schon eingelesen (dieselben Bytes zählen) oder ein anderer Lauf liest sie gerade ein. Wird verarbeitet, wenn der Lauf und alle seine Schreibvorgänge gelingen, und freigegeben, wenn er scheitert. Die Prüfung, mit der ein Import beginnt. |
system.file.processed |
system.file.processed(file_id, key="default") |
True, wenn diese Datei — oder dieselben Bytes unter anderem Namen oder nach einem Verschieben — schon unter key verarbeitet ist, oder ein anderer Lauf sie gerade hält. Ein Blick, keine Sperre — entscheiden Sie mit system.file.claim. |
system.file.mark_processed |
system.file.mark_processed(file_id, key="default", note=None) |
Merkt sich, dass eine Datei erledigt ist. Wird gesammelt wie system.tag.write: abgelegt nach dem Lauf, und nur wenn jeder Schreibvorgang des Laufs durchging. |
system.push.send |
system.push.send(role, title, body, path=None, tag=None) |
Beantragt eine Push-Benachrichtigung an alle, die eine Rolle innehaben — auf dem Telefon und im Browser gleichermaßen. Wird gesammelt wie system.tag.write: der Wirt versendet sie nach dem Lauf, oder gar nicht, im Probelauf. |
system.log.info |
system.log.info(text) |
Eine Zeile für das Diagnose-Panel. Bei einem Lauf, den niemand von Hand gestartet hat, sehen sie die Administratoren, mit dem Pfad des Skripts – alle anderen sehen einen kurzen Hinweis, dass eine Warnung oder ein Fehler protokolliert wurde; bei einem Lauf von Hand und bei Ausprobieren der, der den Knopf gedrückt hat (Ausprobieren zeigt sie zusätzlich unter dem Ergebnis). Bei einem Skript-Tag sehen sie die Administratoren mit dem Pfad des Tags – die Zeilen eines Getters höchstens einmal je Minute, weil er bei jedem Lesen läuft. Bleibt auch erhalten, wenn der Lauf scheitert; höchstens 20 Zeilen je Lauf. |
system.log.warn |
system.log.warn(text) |
Wie system.log.info, Stufe Warnung — etwas ging schief, und das Skript hat weitergemacht. |
system.log.error |
system.log.error(text) |
Wie system.log.info, Stufe Fehler. Der Weg, einen Fehler zu melden, den das Skript selbst abgefangen hat — ein nicht abgefangener verwirft alle Schreibvorgänge des Laufs. |
inputs |
inputs["name"] |
Ihre gebundenen Eingänge, unter den Namen aus der Eingangsliste. Der nachvollziehbare Weg, von einem Tag abzuhängen. |
params |
params["name"] |
Was der Auslöser wusste. Ein Verbindungsereignis übergibt dem Skript den Namen der Verbindung, die abgerissen ist; Datei angekommen übergibt file_id, folder und name der neuen Datei. |
result |
result = ... |
Der Wert, wenn Sie keinen zurückgeben. Nur in der einfachen Form — ein Skript mit Haken antwortet durch sie. |
on_read |
def on_read(): |
Liefert den Wert des Tags. Wer ihn schreibt, verschiebt seinen übrigen Code auf Modulebene, wo er bei jedem Aufruf läuft, vor dem Haken. |
on_write |
def on_write(value): |
Nimmt einen geschriebenen Wert entgegen und macht das Tag überhaupt erst schreibbar. Was der Haken zurückgibt, wird der Wert des Tags. |
print |
print(...) |
Geht ins Ausführungsprotokoll, nicht ins Ergebnis. Die beiden Kanäle sind mit Absicht getrennt. |
Mehr kommt von uns nicht. Alles Übrige in Ihrem Skript ist Python.
Dateien: Lieferungen einlesen, ohne dass jemand klickt
Abschnitt betitelt „Dateien: Lieferungen einlesen, ohne dass jemand klickt“Eine Lieferung, die immer wieder kommt — jeden Morgen ein Zip je Fertigungslinie, ein Export, den
eine Maschine jede Schicht ablegt — liest ein Skript ein, nicht jemand von Hand. system.file
erreicht die Dateiablage (Einstellungen › Dateien); der Auslöser Datei angekommen startet das
Skript für jede neue Datei in einem Ordner, egal wer sie abgelegt hat: jemand auf der Seite
Dateien, eine Maschine mit API-Token, der Assistent oder ein anderes Skript.
import csv, io, zipfile
def run_import(folder): imported = skipped = 0 for f in system.file.list(folder, "*.zip"): if not system.file.claim(f["id"]): skipped += 1 continue with zipfile.ZipFile(system.file.read(f["id"])) as z: for entry in z.namelist(): if entry.endswith(".csv"): text = io.TextIOWrapper(z.open(entry), encoding="utf-8") for row in csv.DictReader(text, delimiter=";"): system.tag.write("Production.Records", {"Line": row["line"], "Pieces": int(row["pieces"])}) imported += 1 system.tag.write("Import.Status", f"{imported} Zeilen eingelesen, {skipped} Lieferungen schon eingelesen")Was ein Skript sieht: die ganze Dateiablage außer den privaten Ordnern Chat/, Users/
und Sessions/. Hinter einem Skript steht keine Person, und dort liegt, was einer Person oder einem
Gespräch gehört — ein Skript kann dort nichts auflisten, lesen, schreiben, verschieben oder
beanspruchen, eine Datei-ID von dort liest sich wie „keine solche Datei“, und ein Auslöser
Datei angekommen kann sie nicht beobachten. Team/ und alle anderen Ordner sind gemeinsam.
Zweimal laufen ist ungefährlich. system.file.claim() nimmt die Datei in einem atomaren
Schritt für diesen Lauf und vergleicht den Inhalt: dieselbe Lieferung noch einmal hochgeladen —
unter anderem Namen, oder nachdem die erste Kopie verschoben wurde — gilt als erledigt, und zwei
Läufe, die im selben Moment auf einer Lieferung starten (das Skript mit Datei angekommen und das
Knopf-Skript), können sie nicht beide bekommen. Der Anspruch wird „verarbeitet“, wenn der Lauf und
alle seine Schreibvorgänge durchgingen, und freigegeben, wenn der Lauf scheitert, sodass der nächste
Lauf sie einliest; der Anspruch eines abgestürzten Laufs verfällt nach seiner Laufzeit plus zehn
Minuten. system.file.mark_processed() markiert eine Datei ohne Anspruch (oder ergänzt eine Notiz),
gesammelt wie ein Schreibvorgang.
Erst an einer echten Lieferung ausprobieren. Ausprobieren (und der Probelauf des Assistenten) liest die echte Datei und legt nichts ab, verschiebt nichts und markiert nichts — das Ergebnis listet, was geschehen wäre.
Grenzen. system.file.write hält sich an Dateigröße und Kontingent der Ablage.
system.file.read ist je Lauf begrenzt, in Bytes (512 MB, sofern die Installation nicht
SCRIPT_FILE_READ_BUDGET_MB setzt) und in Dateien (1.000); Laufzeit-, Speicher- und
Dateigrößen-Grenzen des Laufs gelten zusätzlich. Ein Import braucht meist mehr als die
voreingestellten fünf Sekunden: geben Sie ihm bis zu 60 s.
Ein Knopf auf einem Board braucht kein eigenes Widget: ein Sollwert-Widget schreibt ein Zahlen-Tag, ein Skript mit dem Auslöser Tag-Änderung auf diesem Tag liest ein, und das Skript schreibt seinen Stand („42 Zeilen eingelesen, 3 Dubletten übersprungen“ oder den Fehler) in ein Text-Tag, das eine KPI-Kachel anzeigt. Schreiben Sie den Import einmal als Bibliothek und starten Sie ihn aus beiden Skripten — dem Knopf-Skript und dem mit Datei angekommen.
inputs oder system.tag.read() — nehmen Sie inputs
Abschnitt betitelt „inputs oder system.tag.read() — nehmen Sie inputs“Ein gebundener Eingang ist der nachvollziehbare Weg: TrakState weiß dann, dass dieses Skript an diesem Tag hängt, und kann es Ihnen zeigen.
# Gebunden: der Name links kommt aus Ihrer Eingangsliste.parts = inputs["part_count"]system.tag.read() ist bequem und die schwächere Wahl: nichts außerhalb Ihres Textes weiß,
dass das Skript von diesem Tag abhängt. Wird der Pfad umbenannt, geht das Skript kaputt, und
niemand wird gewarnt.
# Ungebunden: nur nehmen, wenn der Pfad erst zur Laufzeit feststeht.line = params["line"]parts = system.tag.read("Plant." + line + ".PartCount")system.tag.read() scheitert laut, wenn es den Wert nicht gibt — es liefert kein None.
Ein still fehlender Wert wird sonst weiter unten zu einer falschen Zahl.
Schreibungen werden gesammelt, nicht ausgeführt
Abschnitt betitelt „Schreibungen werden gesammelt, nicht ausgeführt“system.tag.write() sendet nichts. Es hängt Ihren Wunsch an eine Liste, die der Wirt
danach abarbeitet — oder eben nicht, und genau das ist ein Probelauf (Dry run):
derselbe Code, dieselben Berechnungen, die Schreibungen unterbleiben.
Zwei Folgen, die Sie kennen sollten:
- Ein
system.tag.write()ist nach dem Aufruf noch nicht passiert. Einsystem.tag.read()auf denselben Pfad in derselben Ausführung liefert weiter den alten Wert. - Die Rechteprüfung bleibt beim Wirt. Ein Skript kann nichts schreiben, was der Auslöser nicht schreiben dürfte.
Push-Benachrichtigungen werden gesammelt, nicht versendet
Abschnitt betitelt „Push-Benachrichtigungen werden gesammelt, nicht versendet“system.push.send() funktioniert genau wie system.tag.write(): es sendet selbst nichts,
sondern hängt Ihren Wunsch an eine Liste, die der Wirt danach abarbeitet — oder im
Probelauf gar nicht, wo das Panel “würde senden” anzeigt und kein Telefon klingelt.
system.push.send("qualitaet", "Presse1 steht", "Die Presse steht seit 5 Minuten.", path="/alarms/presse1", tag="presse1-steht")roleist ein Rollenname, keine Person — jeder, der die Rolle innehat, bekommt die Benachrichtigung, auf jedem registrierten Gerät (Browser-Tab und TrakState-App gleichermaßen).pathist das Ziel, an dem ein Antippen der Benachrichtigung landen soll, relativ zur eigenen Instanz.tagfasst zusammengehörige Benachrichtigungen zusammen, sodass eine spätere eine frühere auf dem Bildschirm ersetzt statt sich anzuhäufen; der Versand nutzt es außerdem, um dieselbe Benachrichtigung nicht zweimal innerhalb einer Minute zu senden.- Ein Lauf ist auf 10 Aufrufe von
system.push.send()begrenzt; der Rest wird als Fehler am Lauf gemeldet statt versendet.
Einen Datensatz schreiben
Abschnitt betitelt „Einen Datensatz schreiben“Ein Skript schreibt einen Datensatz genauso wie einen Wert: über das Tag.
system.tag.write("Process.Records", { "At": system.time.plant_now().isoformat(), "Machine": "ERA-01", "Area": 3.6,})So werden Maschinendaten zu auswertbaren Zeilen: ein Konnektor oder ein anderes Skript legt die Meldung der Maschine auf ein Tag, Ihr Skript macht eine Zeile daraus, und das Datensatz-Tag ist die einzige Tür, durch die sie geht. Direkt in die Datenbank schreibt nichts.
Zwei Dinge, die man kennen sollte — beide kosten einen Nachmittag, wenn man ihnen unvorbereitet begegnet:
- Die Zeile wird Ihrem Skript zugeschrieben, mit seinem Pfad —
script:Decoders.ERA-01— und stattdessen der Person, wenn sie auf Ausführen gedrückt hat. Ein Datensatz ohne Urheber wird abgewiesen, denn eine Zeile ohne Herkunft bricht die Spur, die die Erfassung führen muss. Das stellen Sie nicht ein und können es nicht: es kommt daher, wer oder was tatsächlich gelaufen ist. - Werte müssen einfach sein: Text, Zahlen, wahr/falsch.
system.time.plant_now()liefert eindatetime, und eindatetimein einem geschriebenen Wert übersteht den Weg nicht — schreiben Sie.isoformat()wie oben. Ein Wert, der nicht mitreisen kann, lässt den ganzen Lauf scheitern, nicht nur das Feld.
Die Zeile hält außerdem fest, wie sie hereinkam: script für diese, daneben manual für
etwas, das jemand getippt hat, und assistant für etwas, das der Assistent erfasst hat. Eine
Zahl, die falsch aussieht, lässt sich viel leichter erklären, wenn die Zeile sagt, woher sie kommt.
Die Uhr, nach der die Anlage läuft
Abschnitt betitelt „Die Uhr, nach der die Anlage läuft“system.time.plant_now() liefert die Plantzeit — als datetime, das seine Zone kennt.
jetzt = system.time.plant_now()schichtbeginn = jetzt.replace(hour=6, minute=0, second=0, microsecond=0)datetime.now() ist etwas anderes: die Zeitzone der Maschine, auf der Ihr Skript ausgeführt
wird. Auf einer betriebenen Instanz ist das zuverlässig UTC. Ein Skript, das der Zeitplan um
„Montag 06:00” Plantzeit startet, rechnet dann mit einer anderen Uhr als sein eigener
Auslöser. Solange beide Zonen zufällig übereinstimmen, fällt das nicht auf — es fällt bei der
Schichtübergabe um sechs Uhr morgens auf.
Deshalb ist es eine benannte Funktion und keine Zeitzone im Prozess: eine Umgebungsvariable
hätte datetime.now() still umgedeutet und damit auch jedes Skript geändert, das bewusst UTC
rechnet. Für alles außer der Wanduhr der Anlage bleibt datetime Pythons datetime.
Die KI fragen
Abschnitt betitelt „Die KI fragen“system.ai.ask(prompt, data) stellt dem Sprachmodell eine Frage und liefert die Antwort als Text —
dasselbe Modell, das auch der Assistent benutzt.
figures = system.tag.read("KPI.ShiftSummary")report = system.ai.ask("Fasse diese Schicht in drei Sätzen zusammen und nenne Auffälligkeiten.", figures)system.tag.write("Reports.Records", {"At": system.time.plant_now().isoformat(), "Text": report})Die Zahlen rechnet Ihr Skript, nicht das Modell. Geben Sie ihm in data mit, was eine Abfrage
oder Ihr Code ermittelt hat. Das Modell ist angewiesen, nur diese Zahlen zu verwenden und zu sagen,
wenn etwas fehlt. Einen Bericht, dessen Zahlen ein Modell erfunden hat, glaubt ausgerechnet der, der
nicht nachprüft.
Die Antwort kommt in der Oberflächensprache der Person, der der Lauf gehört — wer „Jetzt starten“ gedrückt oder den auslösenden Wert geschrieben hat, und bei einem Lauf, den niemand angestoßen hat (Intervall, Zeitplan, Ereignisse), wer das Skript zuletzt gespeichert hat; ohne beides Englisch. Die Sprache Ihrer Frage entscheidet das nicht, denn wer das Skript schreibt, liest den Bericht selten.
Drei Dinge, die Sie wissen sollten:
- Nur in Skript-Objekten — auf einem Auslöser oder von Hand gestartet. Ein Skript-Tag wird bei jedem Lesen ausgewertet, und ein Board, das regelmäßig nachliest, würde aus einer Kachel eine Modellrechnung machen. Dort scheitert der Aufruf mit einer Meldung, die das sagt.
- Die Wartezeit auf das Modell zählt nicht zum Zeitlimit. Eine Frage wartet höchstens 60 Sekunden; das Zeitlimit gilt für Ihren eigenen Code.
- Jede Frage kostet. Sie zählt gegen das KI-Budget des Mandanten und steht im Verbrauch unter
script.ask. Mehr als 40 000 Zeichen Daten werden abgelehnt — verdichten Sie vorher in einer Abfrage.
print() gehört Ihnen
Abschnitt betitelt „print() gehört Ihnen“In vielen Skript-Umgebungen ist die Standardausgabe der Ergebniskanal — das erste print(),
mit dem Sie nachsehen wollen, wie ein Wert aussieht, zerstört dort das Ergebnis. Hier nicht:
Ergebnis und Ausgabe laufen getrennt. Ihre Ausgaben stehen im Ausführungsprotokoll unter
dem Lauf.
print("good:", inputs["good"], "scrap:", inputs["scrap"])print() bleibt im Ausführungsprotokoll. Was im Diagnose-Panel ankommen soll – eine
Warnung, ein Fehler, den das Skript selbst abgefangen hat – geht über
system.log.info/warn/error (Tabelle oben).
Was Sie importieren dürfen
Abschnitt betitelt „Was Sie importieren dürfen“Die Standardbibliothek von Python — unter anderem:
json math time datetime zoneinfo calendar decimal fractions statisticsrandom re string textwrap collections itertools functools operatortyping dataclasses enum copy uuid hashlib base64 binascii structcsv io os sys pathlib bisect heapq abcDazu Ihre eigenen Bibliotheken, über ihren Pfad als Modulnamen (siehe Bibliotheken).
Ein Import, der weder eine Ihrer Bibliotheken noch ein bekanntes Standardmodul benennt, wird im Reiter Probleme als Hinweis gemeldet — nicht als Fehler. TrakState kennt die Python-Installation Ihrer Anlage nicht und behauptet deshalb nie, ein Modul gebe es nicht; es sagt nur, dass es hier nicht gefunden wurde.
system müssen Sie nicht importieren. Es steht bereit, bevor Ihre erste Zeile läuft.
Grenzen
Abschnitt betitelt „Grenzen“| Zeitlimit je Lauf | 5 Sekunden |
| Wartezeit auf das Modell | zählt nicht zum Zeitlimit; höchstens 60 Sekunden je Frage |
| Arbeitsspeicher | 256 MB (auf Linux; unter Windows nicht erzwingbar) |
| Ausgabemenge | begrenzt — eine Schleife voller print() kann den Speicher nicht füllen |
| Rückgabewert | muss sich als JSON darstellen lassen |
Läuft ein Skript in das Zeitlimit, endet der Lauf mit „the script was stopped after 5000 ms”. Das ist kein Fehler in Ihrem Code, sondern seine Laufzeit.
Offen: Das Zeitlimit ist je Skript hinterlegbar, aber die Oberfläche bietet dafür derzeit kein Feld. Bis das nachgezogen ist, gelten die 5 Sekunden.
Wenn etwas schiefgeht
Abschnitt betitelt „Wenn etwas schiefgeht“Ein Fehler wird als Typ, Meldung und Zeilennummer in Ihrem Code gemeldet — nicht in
unserem. Auch wenn system.tag.read() scheitert, zeigt die Meldung auf die Zeile, in der
Sie es aufgerufen haben.
ZeroDivisionError: division by zero (Zeile 4)