Troubleshooting - HeikoGr/MMM-Webuntis GitHub Wiki
Check these first:
- Verify the Node version is at least
22.22.1. - Confirm the auth method matches the account type.
- Set
logLevel: 'debug'(and addDEBUGto the globallogLevelinconfig.js). - For parent setups, confirm
students: []is present if you expect auto-discovery. - Make sure the selected date range actually contains timetable data.
The backend owns the fetch schedule. After a failed fetch (wrong password, server unreachable) it retries after 2 minutes, then 4, 8 … up to 30 minutes, and an instance stays alive for 10 minutes after its last display went away. Reloading the browser therefore does not force an immediate new login attempt; restart MagicMirror (or wait for the next retry) after fixing the credentials.
The backend keeps one WebUntis session per account. The bearer token lives about 15 minutes, and the
module renews it from the session cookie shortly before it expires instead of logging in again (debug
line token renewed from the existing session); a real login follows when that no longer works and at
the latest after six hours. logLevel: 'info' shows one line per real login (REST auth: logging in ...).
How many logins you see depends on how busy the account is: the WebUntis REST session idles out after
about five minutes without a request. With several instances or Carousel pages of one account the
session stays warm and you see about one login per 15 minutes. A single instance stays warm with the
default of 4 minutes as well (the longest gap is 4:24 with the ±10 % jitter). With an updateInterval of
5 minutes or more a single instance sits on or beyond that limit: on many fetches it starts with a 401
(Authentication token expired, invalidating cache and retrying... in the debug log) and recovers with a
login. That still works, it only costs one extra request and a login per fetch.
If a login fails, the module waits 30 seconds before it tries again with the same credentials, and a
401 no longer keeps the timetable away afterwards. When a login replaces a session that had only aged
out, the old one is logged out in the background, and on shutdown every cached session is.
All output goes through MagicMirror's Log: the global logLevel in config.js is the upper
bound, the module's logLevel can only narrow it. Debug lines need DEBUG in the global list.
logLevel: 'info' is the level for "data disappears now and then". It stays quiet during normal
operation and prints exactly two kinds of auth line:
- one line per real login (
REST auth: logging in ...orQR Auth: Starting authentication ...) - cached sessions are reused silently, so every line here is an actual login - one line per invalidated session (
Invalidating expired token cache)
A login without a preceding invalidation means the token simply aged out. A login right after an invalidation means something rejected the session. Correlating those timestamps with the moment data vanished is usually enough to tell a session problem from an empty timetable.
logLevel: 'debug' adds cache hits, per-request details and payload summaries - useful once you
know where to look, too noisy to watch for hours.
An empty timetable view does not always mean that there is really no school on that day.
Current runtime behavior to keep in mind:
- the backend uses a timetable-first fetch strategy because timetable is the auth canary
- the frontend can preserve older data when a new fetch fails critically
- if no usable timetable data remains for a day, the grid plugin falls back to
no-lessons - the lessons plugin may render its empty row text even though the interesting question is really whether data was unavailable or the day was genuinely empty
For investigations, verify these separately:
- Was timetable fetching attempted at all?
- What does the API status snapshot report for
timetable? - Were warnings or
warningMetaentries emitted for auth, network, or server errors? - Does the selected date range actually include lessons?
- Do debug dumps or PM2 logs show an upstream empty response, a timeout, or an auth refresh?
With dumpBackendPayloads: true, open the newest debug_dumps/<timestamp>_<student>_api.json
and look at state.collections first - it says per collection whether the latest fetch succeeded
(ok), failed or was skipped (unavailable) or was not requested (disabled), and when the last
success was (lastSuccessAt). Then state.fetch, state.api, state.warnings, data.lessons
and data.dayNotices:
| State | state.fetch.timetable |
state.api.timetable |
state.warnings |
data.lessons |
What the widgets show |
|---|---|---|---|---|---|
| Timetable never requested |
false (no grid/lessons plugin) or true with nextDays: 0
|
null |
– | [] |
plugin not rendered, or rendered empty |
| Fetch failed (network, 5xx, 401) | true |
0, 5xx or 401
|
network/auth/server warning | [] |
previous data is kept if the session had any; otherwise kein Unterricht plus a warning banner |
| Endpoint forbidden (403, remembered for 24h) | true |
403 |
none | [] |
previous data is kept if any; otherwise kein Unterricht without a warning |
| Session cookie expired (idle > ~5 min) and the automatic re-login failed | true |
401 |
auth warning (session expired / Authentication failed) |
[] |
previous data is kept and state.collections.lessons.status is unavailable; a fresh session shows Daten nicht verfügbar per day plus the warning banner |
Timetable body without days[] (maintenance page, proxy error) |
true |
0 |
unusable response warning |
[] |
previous data is kept; without previous data: Daten nicht verfügbar
|
Plan locked by the school (NOT_ALLOWED) |
true |
200 |
none | [] |
Plan gesperrt (from data.dayNotices[].kind = timetable-restricted) |
| Genuinely no lessons | true |
200 |
none | [] |
kein Unterricht; WebUntis marks such days NO_DATA, forwarded as data.dayNotices[].kind = no-data
|
Plan gesperrt and a genuinely lesson-free day both come with state.collections.lessons.status = ok; the raw raw_api_*_timetable.json dump written by dumpRawApiResponses: true shows the
per-day status (NO_DATA vs. NOT_ALLOWED). Failed requests are dumped as
raw_api_*_<type>_error.json with status, error code, redirect location and a body snippet.
Useful references:
docs/SERVER_REQUEST_FLOW.mddocs/API_V3_MANIFEST.mddocs/ARCHITECTURE.md
- Check that the QR code still matches the right account.
- Prefer QR for SSO-backed accounts.
- Re-scan the code if the original value was copied manually.
- Re-check
schoolandserver. - Make sure the account really supports direct login.
- If the school uses SSO, switch to QR.
- Check
displayModefirst. - Then review plugin-specific
plugins.<id>.configsettings. - Test with a simpler mode such as
lessons, exams.
node --run check
node --run debug
node --run test:auth:curl
pm2 logs --lines 100The repository still contains technical diagnostics and internals in docs/, especially:
docs/CLI.mddocs/API_REFERENCE.mddocs/SERVER_REQUEST_FLOW.md
If you are debugging module internals rather than just setting it up, start there.