Examples · Internet of Things
A gateway log, explained
Sixty-five lines from the log of a classroom IoT gateway go in, and incidents come out as JSON, each with the lines it is built on. Your code checks that every line exists and every quote is on it, lists the warnings left out, and only then asks for a short note for the technician.
Intermediate · 35 min
The idea
A gateway collects the readings of the sensors in a room and sends them to the broker. It also writes a log: hundreds of lines a day, most of them “everything is fine”. When something goes wrong, the useful lines are scattered among the others, and one problem often shows up as ten lines in three places.
A model reads the whole log and groups the lines into incidents. That is the useful part, and also the risky one: it can point at the wrong line, quote something that is not there, or leave a warning out. So:
- the model groups the events into incidents, as JSON, with the line numbers and a quote from each line;
- your code checks every line number and every quote, and lists any warning or error that is in no incident;
- only the lines that passed go into a second request: a short note for the technician.
What you need
A key and the openai library, as in Your first API call in Python.
The log
Two hours of gw-lab3, the gateway of Lab 3 in Legnano, on a Wednesday morning. Save it as gateway.log. Read it before you run anything: how many problems do you find?
2026-10-07 07:58:01 INFO [system] gw-lab3 running, firmware 2.3.0, uptime 6d 21h
2026-10-07 07:58:01 INFO [mqtt] connected to broker 10.20.0.5:8883 (tls)
2026-10-07 07:59:12 INFO [zigbee] air-12 report: 21.4 C, 47 %, battery 2.41 V
2026-10-07 07:59:12 WARN [zigbee] air-12 battery low (2.41 V < 2.50 V)
2026-10-07 08:00:00 INFO [bridge] forwarded 24 readings in the last 5 min
2026-10-07 08:00:03 INFO [zigbee] door-3 report: open
2026-10-07 08:02:44 DEBUG [wifi] signal -67 dBm
2026-10-07 08:05:00 INFO [bridge] forwarded 23 readings in the last 5 min
2026-10-07 08:06:30 INFO [dhcp] lease renewed: 10.20.3.41 for 12 h
2026-10-07 08:07:15 WARN [tls] broker certificate expires in 6 days (2026-10-13)
2026-10-07 08:08:51 INFO [zigbee] door-3 report: closed
2026-10-07 08:10:00 INFO [bridge] forwarded 24 readings in the last 5 min
2026-10-07 08:11:52 DEBUG [wifi] signal -79 dBm
2026-10-07 08:12:07 WARN [wifi] wlan0 disconnected (reason 4: inactivity)
2026-10-07 08:12:07 ERROR [mqtt] connection lost: network unreachable
2026-10-07 08:12:08 INFO [bridge] broker offline, queueing readings
2026-10-07 08:12:12 INFO [mqtt] reconnect attempt 1 in 5 s
2026-10-07 08:12:22 INFO [mqtt] reconnect attempt 2 in 10 s
2026-10-07 08:12:42 INFO [mqtt] reconnect attempt 3 in 20 s
2026-10-07 08:13:02 INFO [mqtt] reconnect attempt 4 in 40 s
2026-10-07 08:13:38 INFO [wifi] wlan0 connected to ITS-LAB, signal -70 dBm
2026-10-07 08:13:43 INFO [mqtt] connected to broker 10.20.0.5:8883 (tls)
2026-10-07 08:13:43 INFO [bridge] sent 18 queued readings
2026-10-07 08:15:00 INFO [bridge] forwarded 19 readings in the last 5 min
2026-10-07 08:15:31 DEBUG [ble] ignored advertisement from 4C:21:9E:0A:77:12 (unknown device)
2026-10-07 08:20:00 INFO [bridge] forwarded 24 readings in the last 5 min
2026-10-07 08:20:04 WARN [zigbee] air-12 no answer to poll (timeout 3 s), retry 1/5
2026-10-07 08:22:10 DEBUG [ble] ignored advertisement from 4C:21:9E:0A:77:12 (unknown device)
2026-10-07 08:25:00 INFO [bridge] forwarded 22 readings in the last 5 min
2026-10-07 08:25:04 WARN [zigbee] air-12 no answer to poll (timeout 3 s), retry 2/5
2026-10-07 08:30:00 INFO [bridge] forwarded 22 readings in the last 5 min
2026-10-07 08:30:04 WARN [zigbee] air-12 no answer to poll (timeout 3 s), retry 3/5
2026-10-07 08:31:47 INFO [zigbee] door-3 report: open
2026-10-07 08:35:00 INFO [bridge] forwarded 22 readings in the last 5 min
2026-10-07 08:35:04 WARN [zigbee] air-12 no answer to poll (timeout 3 s), retry 4/5
2026-10-07 08:40:00 INFO [bridge] forwarded 22 readings in the last 5 min
2026-10-07 08:40:04 WARN [zigbee] air-12 no answer to poll (timeout 3 s), retry 5/5
2026-10-07 08:40:04 ERROR [zigbee] air-12 marked offline after 5 failed polls
2026-10-07 08:45:00 INFO [bridge] forwarded 20 readings in the last 5 min
2026-10-07 08:50:00 INFO [bridge] forwarded 20 readings in the last 5 min
2026-10-07 08:55:00 INFO [bridge] forwarded 20 readings in the last 5 min
2026-10-07 09:00:00 INFO [update] firmware 2.3.1 available
2026-10-07 09:00:02 INFO [update] downloading gw-fw-2.3.1.bin (14.2 MB)
2026-10-07 09:01:15 INFO [update] download complete, sha256 OK
2026-10-07 09:01:16 INFO [update] installing, the gateway will restart
2026-10-07 09:01:40 INFO [system] rebooting
1970-01-01 00:00:09 INFO [system] gw-lab3 running, firmware 2.3.1, uptime 0d 0h
1970-01-01 00:00:11 WARN [config] unknown key mqtt.keep_alive, using the default (60 s)
1970-01-01 00:00:12 INFO [wifi] wlan0 connected to ITS-LAB, signal -69 dBm
1970-01-01 00:00:14 ERROR [mqtt] TLS handshake failed: certificate is not yet valid
1970-01-01 00:00:15 INFO [zigbee] network up, 6 of 7 devices answered (missing: air-12)
1970-01-01 00:00:19 INFO [mqtt] reconnect attempt 1 in 5 s
2026-10-07 09:02:31 INFO [ntp] clock set from pool.ntp.org (it was 1970-01-01 00:00:21)
2026-10-07 09:02:33 INFO [mqtt] connected to broker 10.20.0.5:8883 (tls)
2026-10-07 09:05:00 INFO [bridge] forwarded 17 readings in the last 5 min
2026-10-07 09:10:00 INFO [bridge] forwarded 20 readings in the last 5 min
2026-10-07 09:12:28 DEBUG [ble] ignored advertisement from 4C:21:9E:0A:77:12 (unknown device)
2026-10-07 09:15:00 INFO [bridge] forwarded 20 readings in the last 5 min
2026-10-07 09:40:58 DEBUG [wifi] signal -78 dBm
2026-10-07 09:41:10 WARN [wifi] wlan0 disconnected (reason 4: inactivity)
2026-10-07 09:41:10 ERROR [mqtt] connection lost: network unreachable
2026-10-07 09:41:15 INFO [mqtt] reconnect attempt 1 in 5 s
2026-10-07 09:41:29 INFO [wifi] wlan0 connected to ITS-LAB, signal -78 dBm
2026-10-07 09:41:30 INFO [mqtt] connected to broker 10.20.0.5:8883 (tls)
2026-10-07 09:41:30 INFO [bridge] sent 3 queued readings
The program
Save it as explain_log.py, next to the log:
import json
import os
import re
import sys
from openai import OpenAI
client = OpenAI(
base_url="https://api.ai.itsincom.org/v1",
api_key=os.environ["API_KEY"],
)
model = sys.argv[1] if len(sys.argv) > 1 else "qwen3.8-27b"
lines = open("gateway.log").read().splitlines()
numbered = "\n".join(f"{n:3} | {line}" for n, line in enumerate(lines, 1))
schema = {
"type": "object",
"properties": {
"incidents": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"severity": {"type": "string", "enum": ["high", "medium", "low"]},
"what_happened": {"type": "string"},
"evidence": {
"type": "array",
"items": {
"type": "object",
"properties": {
"line": {"type": "integer"},
"quote": {"type": "string"}, # copied from that line, word for word
},
"required": ["line", "quote"],
},
},
},
"required": ["title", "severity", "what_happened", "evidence"],
},
},
},
"required": ["incidents"],
}
answer = client.chat.completions.create(
model=model,
temperature=0,
messages=[
{"role": "system", "content": "You read the log of an IoT gateway for the technician. Group the events "
"into incidents: one incident is one problem, with all the lines that "
"belong to it, from the first sign to the end. Leave out what is normal. "
"For every line you use, copy a short quote from it, word for word."},
{"role": "user", "content": numbered},
],
response_format={"type": "json_schema", "json_schema": {"name": "incidents", "schema": schema}},
)
if answer.choices[0].finish_reason != "stop":
sys.exit("The answer was cut off: run it again.")
incidents = json.loads(answer.choices[0].message.content)["incidents"]
# Check 1: every line exists, and the quote is really on it.
used = set()
for incident in incidents:
checked = []
for e in incident["evidence"]:
n, quote = e["line"], e["quote"].strip()
if not 1 <= n <= len(lines):
print(f" line {n} does not exist")
elif not quote or quote not in lines[n - 1]:
print(f" line {n}: «{quote}» is not on that line")
else:
checked.append(n)
incident["lines"] = sorted(set(checked))
used.update(checked)
print(f"[{incident['severity']}] {incident['title']} (lines {', '.join(map(str, incident['lines']))})")
print(f" {incident['what_happened']}")
# Check 2: every warning and every error belongs to an incident.
for n, line in enumerate(lines, 1):
if (" WARN " in line or " ERROR " in line) and n not in used:
print(f"NOT IN ANY INCIDENT: {n:3} | {line}")
# The note for the technician, written only from the checked incidents and their real lines.
facts = [{"title": i["title"], "severity": i["severity"], "lines": [lines[n - 1] for n in i["lines"]]}
for i in incidents if i["lines"]]
answer = client.chat.completions.create(
model=model,
temperature=0,
messages=[
{"role": "system", "content": "Write a note for the technician who goes to the lab tomorrow morning: "
"at most 5 short lines, plain text, the most urgent first, each with "
"what to do. Use only these incidents."},
{"role": "user", "content": json.dumps(facts, indent=1)},
],
)
note = answer.choices[0].message.content.strip()
print("\nNote for the technician:\n" + note)
# Check 3: at most 5 lines, and every time in the note is a time in the log.
if len(note.splitlines()) > 5:
print(f"TOO LONG: {len(note.splitlines())} lines")
for time in re.findall(r"\b\d{2}:\d{2}\b", note):
if not any(f" {time}:" in line for line in lines):
print(f"NOT IN THE LOG: {time}")
Run it with python3 explain_log.py. With qwen3.8-27b it takes about a minute: the model reasons before each of the two answers.
What you should see
One of our runs:
[medium] air-12 battery low, then device goes offline (lines 4, 27, 30, 32, 35, 37, 38, 51)
The air-12 sensor's battery dropped below the 2.50 V threshold, and roughly 20 minutes later the device stopped answering polls. After five consecutive failed polls it was marked offline. It was still missing when the gateway came back up after the firmware update.
[medium] WiFi drop → MQTT connection lost → recovery (first occurrence) (lines 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
The WiFi signal degraded from -67 dBm to -79 dBm, then the interface dropped due to inactivity. The MQTT connection was lost, readings were queued, and four reconnect attempts were made over about 90 seconds. WiFi and MQTT both recovered, and the 18 queued readings were flushed.
[low] Broker TLS certificate expiring in 6 days (lines 10)
A warning that the broker's TLS certificate will expire on 2026-10-13, six days after this log entry. No action was taken in the log.
[medium] Post-restart: clock reset → TLS handshake failure → recovery (lines 48, 50, 52, 53, 54)
After the firmware update and reboot, the system clock was stuck at 1970-01-01. This caused the TLS handshake with the broker to fail (certificate 'not yet valid' because the clock was in the past). One reconnect attempt was made. The NTP service then corrected the clock, and the MQTT connection was established. A minor config warning about an unknown key also appeared at startup.
[medium] WiFi drop → MQTT connection lost → recovery (second occurrence) (lines 59, 60, 61, 62, 63, 64, 65)
The WiFi signal degraded to -78 dBm and the interface dropped again due to inactivity. MQTT was lost, one reconnect attempt was made, and both WiFi and MQTT recovered within about 20 seconds. Three queued readings were flushed.
Note for the technician:
Replace battery on air-12 (2.41 V, device offline after 5 failed polls).
Fix recurring WiFi inactivity drops (wlan0 disconnected twice, reason 4) – check AP or raise keep-alive.
Add NTP sync to boot sequence (clock reset to 1970 caused TLS handshake failure until 09:02).
Renew broker TLS certificate (expires 2026-10-13, 6 days out).
Good work, and every line passed the checks. Look at the battery: the warning on line 4 and the timeouts from line 27 are about 20 minutes apart, and the model put them together. Line 51, after the reboot, says that air-12 is still missing: it found that too.
The clock is the most interesting one. After the reboot the gateway thinks it is 1 January 1970, so the broker's certificate is “not yet valid”, and the connection fails until NTP sets the clock. The model explained it correctly in all six of our runs, with both models.
What the checks caught
A warning left out. We ran the program three times with gemma-4-26b. Every time, it left line 48 out of all incidents:
NOT IN ANY INCIDENT: 48 | 1970-01-01 00:00:11 WARN [config] unknown key mqtt.keep_alive, using the default (60 s)
After the update, the gateway ignores a setting that someone chose on purpose. Maybe it does not matter; the technician should decide, not the model. Qwen3.8 put the line in an incident in all three runs; in one of them it called it “minor”.
Line numbers. In our six runs, every line number existed and every quote was on its line. That is because the program sends the lines with their numbers. We tried once without them, with each model:
line 20261007 does not exist
Gemma used the date as a line number, for every line. Qwen3.8 counted the lines itself and lost count:
line 26: «air-12 no answer to poll (timeout 3 s), retry 1/5» is not on that line
That quote is on line 27. Without the check, the technician would open the log at the wrong line. Without the numbers, the check rejected most of the lines: the numbers are what make the answer usable.
What nobody caught
- A silence. The bridge writes “forwarded … readings” every 5 minutes, from 08:00 to 09:15. Then nothing until the Wi-Fi drop at 09:41: five reports missing, and no warning. None of our six runs said a word about it. No rule of the program can see it either, because there is no line to point at. A missing line needs a rule of its own, like the gap rule in Anomalies in sensor readings.
- A number in the note. In one Qwen run the note said: “After reboot the clock was 1970, breaking TLS for ~45 min until NTP synced”. The gateway restarted at 09:01:40 and was back on the broker at 09:02:33: less than a minute. Check 3 looks only at times like “09:02”, and “~45 min” is not one.
- The severity. Qwen3.8 rated the certificate “low” in all three runs, Gemma “medium”. On 13 October the gateway will not be able to connect to the broker at all. The severity is the model's opinion: the date is a fact, and a rule can count the days.
Gemma or Qwen
gemma-4-26b |
qwen3.8-27b |
|
|---|---|---|
| Time for the whole program | about 12 s | 68 to 81 s |
| Warnings left out of every incident | line 48, in 3 runs out of 3 | none |
| Battery and timeouts in one incident | 0 runs out of 3 | 2 runs out of 3 |
| The note | 3 or 4 short lines | 4 or 5 lines, with values such as 2.41 V |
Both models passed check 1 in every run. Gemma is about six times faster; Qwen3.8 connects more events into one incident. For a log you read every morning, run Gemma and let check 2 point at what it left out.
Good to know
- Number the lines yourself. A model cannot count 65 lines reliably. With the numbers in front, it copies them.
- Print the real line, not the quote. The quote is only for the check. The technician reads the log.
- The note is written from checked lines only. A line that failed the check never reaches the second request, so the note cannot be built on it.
- Logs are long. At this rate, a day of this gateway is several hundred lines. Send the model only the WARN and ERROR lines, with a few lines around each, and keep the rest for the rules.
Exercise
- Write the rule for the silence: the bridge must report every 5 minutes. Print every gap longer than 6 minutes. Careful with the lines from 1970.
- Add a rule for the certificate: read the date in line 10, count the days to it, and make it “high” under 14 days, whatever the model says.
- Add a line of your own, a
WARN [disk] /var/log 91 % fullat 09:10. Which model puts it in an incident? - Ask for the note in Italian, for a technician who does not read English. Does check 3 still work?
