Skip to content

EasyIQ Ugeplan: port to skoleportal.easyiqcloud.dk + widget 0128#354

Closed
spraot wants to merge 1 commit into
scaarup:mainfrom
spraot:easyiq-skoleportal-port
Closed

EasyIQ Ugeplan: port to skoleportal.easyiqcloud.dk + widget 0128#354
spraot wants to merge 1 commit into
scaarup:mainfrom
spraot:easyiq-skoleportal-port

Conversation

@spraot
Copy link
Copy Markdown

@spraot spraot commented May 11, 2026

Summary

  • Replaces the old api.easyiqcloud.dk/api/aula/weekplaninfo POST flow with the skoleportal.easyiqcloud.dk AuthenticateAulaUser + Calendar/CalendarGetWeekplanEvents flow that Aula's web UI itself uses.
  • Adds widget 0128 ("EasyIQ Ugeplan") to the EasyIQ detection alongside the legacy 0001; whichever the user has provisioned routes through the same code path.
  • Turns the silent except KeyError: _LOGGER.debug("None") into a WARNING that names the missing key and the response keys it did see, and surfaces non-200 / non-JSON responses at WARNING level too — future schema regressions stay visible at default log level instead of disappearing into a debug("None") haystack.

Why

For a school whose Aula widget list contains 0128 but not 0001, the integration was rendering only <h2> Uge NN</h2> for every child. Debug logging showed every call to api.easyiqcloud.dk/api/aula/weekplaninfo returning:

{"ErrorCode": "1", "ErrorDescription": "Institutionen har ikke licens til EasyIQ Ugeplan widget."}

— for all institutionFilter values, even though the Ugeplan widget itself renders populated content in the Aula UI for the same user. Capturing the live web app traffic in a HAR confirmed the UI never hits api.easyiqcloud.dk at all. It uses a different host with a different auth model:

POST https://skoleportal.easyiqcloud.dk/Aula/AuthenticateAulaUser
     (empty body, Content-Length: 0) -> {"loginId": ..., "child": ..., ...}
GET  https://skoleportal.easyiqcloud.dk/Calendar/CalendarGetWeekplanEvents
     ?loginId=<id>&date=<Monday-of-week, ISO>&activityFilter=-1
     &courseFilter=-1&textFilter=&ownWeekPlan=false

Auth on every skoleportal call (no csrfp-token, no cookies once these are right):

Authorization: Bearer <aulaToken.getAulaToken token, widgetId=0001 or 0128>
X-Login: <parent uniLogin>
X-InstitutionFilter: <comma-separated institution codes>
X-UserProfile: guardian
X-ChildFilter: <comma-separated child uniLogins>
X-Child: <current child uniLogin>
X-Requested-With: Fetch

A realistic User-Agent is required — Cloudflare drops requests without one.

Returned events use StartTime / EndTime already in %Y/%m/%d %H:%M, plus CoursesDisplay (subject name) and Description (HTML body), so the rendered HTML carries over with just field-name swaps. ISO-week → Monday conversion uses datetime.date.fromisocalendar(year, week, 1).

Test plan

  • Three children across two institutions, widget 0128 only: ugeplan and ugeplan_next sensor attributes now render the full HTML body (Mandag 08:00- 08:45 / Musik / <description> …) instead of the empty <h2> header.
  • Verified EasyIQ result for <child> week <YYYY-WNN> (<N> events) debug lines on each coordinator refresh.
  • Not tested against a user with the legacy widget 0001; auth shape is the same and the token API call passes the widget id through, so it should work, but a second pair of eyes from someone still on 0001 would be ideal.
  • Not tested with a guardian whose children are split across schools where only some are on EasyIQ — the per-child loop will warn and continue on the ones that don't authenticate, leaving others intact.

Notes for review

  • The bare except KeyError removed in this PR was the root cause of how long this took to diagnose for me — it would silently swallow the EasyIQ license error and render an empty header. Even if the host change is contentious, the louder error handling stands on its own.
  • EASYIQ_API constant in const.py is now unused inside client.py but I left it in place to avoid breaking anyone importing it; happy to drop in a follow-up if you'd prefer.

The old EasyIQ flow POSTed to api.easyiqcloud.dk/api/aula/weekplaninfo,
which now returns "Institutionen har ikke licens til EasyIQ Ugeplan
widget" for schools whose Aula widget list contains 0128 (EasyIQ
Ugeplan) rather than 0001. Captured the live web app traffic via HAR
and confirmed it uses a different host and endpoint set:

  POST https://skoleportal.easyiqcloud.dk/Aula/AuthenticateAulaUser
       (empty body, Content-Length: 0) -> {"loginId": ...}
  GET  https://skoleportal.easyiqcloud.dk/Calendar/CalendarGetWeekplanEvents
       ?loginId=...&date=<Monday-of-week>&activityFilter=-1
       &courseFilter=-1&textFilter=&ownWeekPlan=false

Auth is via bearer + X-Login / X-InstitutionFilter / X-UserProfile /
X-ChildFilter / X-Child headers; no csrfp-token. Returns a flat events
array with StartTime/EndTime already in the %Y/%m/%d %H:%M format
the renderer expects, plus CoursesDisplay (subject) and Description.

Also widens widget detection so both 0001 and 0128 trigger the
EasyIQ path, replaces the bare except KeyError that silently dropped
all events on schema/auth changes with a warning that names the
missing key, and surfaces non-200 / unexpected-shape responses at
WARNING level so future regressions don't disappear into a
debug("None") haystack.

Verified locally against three children at two institutions: the
ugeplan sensor attribute now renders the full HTML body instead of
just the <h2> Uge NN</h2> header.
@spraot
Copy link
Copy Markdown
Author

spraot commented May 11, 2026

Closing in favour of #352, which lands the same SkolePortal port with a cleaner shape: purely additive (separate if "0128" block, doesn't touch the legacy 0001 path), module-level helpers that make the flow easier to review and reuse, native <details>/<summary> per-day rendering, and html.unescape() so Danish characters render correctly. I didn't notice #352 was already open when I sent this — apologies for the duplicate.

The one delta worth preserving — louder error handling around the silent except KeyError: _LOGGER.debug("None") in the legacy 0001 block — I've left as a comment on #352 (#352 (comment)) for the author/maintainer to fold in if useful.

@spraot spraot closed this May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant