Add EasyIQ SkolePortal support (widget 0128)#352
Conversation
Schools licensed for EasyIQ SkolePortal expose widget 0128 instead of the classic 0001 widget. SkolePortal uses skoleportal.easyiqcloud.dk with a different auth flow (AuthenticateAulaUser → loginId → CalendarGetWeekplanEvents) and PascalCase event fields. This adds a new ugeplan flow for widget 0128 alongside the existing 0001/0029/0004/0062 implementations. Per-child iteration supports families with multiple children at SkolePortal schools. Output uses native HTML <details>/<summary> for browser-collapsible per-day view. Refs scaarup#245
Events from CalendarGetWeekplanEvents are returned in API order, which is not necessarily chronological. Within a single day this caused lessons to render out of order (e.g. a 09:50 lesson appearing after a 12:50 lesson). Sort events by StartTime within each day, and iterate days in Mon-Sun order regardless of insertion order in the by_day dict.
) Adds EasyIqSkoleportalClient — a separate plugin from the existing EasyIqClient (widget 0001). Same vendor, distinct SaaS backend at skoleportal.easyiqcloud.dk with a different auth handshake and a PascalCase event JSON shape. Per-call flow (matches the upstream PR): 1. Get widget token for 0128 via WidgetTokenManager. 2. POST /Aula/AuthenticateAulaUser per child with x-childfilter / x-institutionfilter / x-login headers; receive { loginId, child, childName, schoolName, schoolId }. 3. GET /Calendar/CalendarGetWeekplanEvents?loginId=…&date=YYYY-MM-DD; receive an array of events (StartTime, StartTimeISO, EndTime, CoursesDisplay, ActivitiesDisplay, ChapterTitle, Description). Multi-child: iterate per child (each child has its own loginId). Per-child failures collected as warnings; partial results return. Helpers added in integrations/types.ts: - isoWeekToMonday(isoWeek): inverse of isoWeekString — returns the Monday 00:00 UTC of the given ISO week string. - isoDate(d): YYYY-MM-DD UTC formatter. - decodeHtmlEntities(s): decodes the Danish-specific entities (æ/ø/å + uppercase) plus the standard five and numeric refs. SkolePortal in particular sends un-decoded entities in titles. Wired into MCP: - aula-context.ts: AulaContext.getEasyIqSkoleportal() - tools.ts: aula.ugeplan.easyiq_skoleportal tool - discover.ts already maps widget 0128 → easyiq_skoleportal — agents that see '0128' in detectedWidgets get the right tool listed first.
Five real bugs fixed against live MitID + live Aula traffic; everything from MitID app approval through Aula token exchange and EasyIQ SkolePortal weekplan now works end-to-end. MitID - Drop the speculative /complete path entirely; APP completion runs /init → /prove → /verify → /next exactly like Python's reference. - Pass realistic frontEndProcessingTime (wall-clock ms over the SRP + AES window) instead of the placeholder 100. - Stop PKCS#7-padding the AES-GCM plaintext before /verify. Python's pad() is a no-op (b64decode silently strips the chars); GCM doesn't need block padding. Sending padded bytes was passing /verify-204 but failing the next /next with a generic "Try again." SAML / Aula - Follow redirects on POST /login/mitid (was leaving us on a 302 stub and trying to parse "Object moved" HTML as a SAML form). - POST the final SAMLResponse to the form's own action URL (e.g. …/saml2-acs.php/app-level3-sp), not a hardcoded /uni-sp. Wrong SP endpoint produced SimpleSAMLphp's generic "Unhandled exception." EasyIQ SkolePortal (widget 0128) - Match upstream PR scaarup/aula#352: per-child opaque userId in x-childfilter, skoleportal.easyiqcloud.dk origin/referer, desktop user-agent + x-requested-with + x-userprofile, accept */*, and the YYYY-MM-DDT00:00:00.000Z date format. Authorization stays Bearer <jwt> (Python's get_token prepends "Bearer " before storing, so PR #352's `authorization: token` is the same wire shape). - Plumb childUserIds[] alongside childIds[] through the integration context so SkolePortal can resolve the right child without burning a numeric-vs-opaque guess. MCP server / discover - Read widgetConfigurations[].widget.widgetId (nested), with a fallback to the legacy flat shape. We were reading the flat path, so detectedWidgets came back empty and Claude fanned out across every ugeplan tool. - Treat profileContext userId / child userId as opaque (string | number) — Aula returns alphanumeric tokens for some accounts. - Tighten capabilities so detected vendors are the only listed tool; no alternates to invite fan-out. - Add usage block + workflow instructions (cache discover, resolve names, pick one tool, default ranges, language) so Claude needs fewer round-trips to behave well. - Server reads the same KeychainTokenStore the CLI writes to, and bumps Bun.serve idleTimeout to 240s for SSE.
|
Drive-by suggestion while you're in the EasyIQ neighborhood: the legacy widget try:
for i in ugeplaner.json()["Events"]:
...
except KeyError:
_LOGGER.debug("None")When the EasyIQ API returns an error envelope (e.g. Two-line fix to surface it loudly: _LOGGER.debug("EasyIQ Opgaver response " + str(ugeplaner.json()))
easyiq_payload = ugeplaner.json()
if isinstance(easyiq_payload, dict) and easyiq_payload.get("ErrorCode"):
_LOGGER.warning(
"EasyIQ %s rejected weekplan request for %s "
"(institution header %s, week %s): %s %s",
"0001", first_name, self._institutionProfiles[0], week,
easyiq_payload.get("ErrorCode"), easyiq_payload.get("ErrorDescription"),
)
# ... existing _ugep build, findDay, is_correct_format ...
try:
for i in easyiq_payload["Events"]:
...
else:
_LOGGER.debug(
"EasyIQ event for %s skipped: unrecognised start format %r",
first_name, i.get("start"),
)
except KeyError as exc:
_LOGGER.warning(
"EasyIQ weekplan for %s missing expected key %s; response keys=%s",
first_name, exc,
list(easyiq_payload.keys()) if isinstance(easyiq_payload, dict)
else type(easyiq_payload).__name__,
)Same UX for genuine empty weeks; just makes EasyIQ license/auth errors and future schema changes visible at default log level instead of behind a Independent of this — I opened #354 yesterday with a SkolePortal port that I now realise duplicates this PR. Closing mine in favour of yours, since the additive shape, helpers, |
|
Thanks @spraot — much appreciated, both for closing #354 and for the kind words on the structure. 🙏 The drive-by suggestion is a good catch. I ran into exactly that silent failure during the SkolePortal reverse-engineering earlier this month — the bare I'd love to keep this PR focused on the additive 0128 path, so a separate small PR from you for the 0001 error-surfacing fix would be perfect. Happy to test it on my end before/after to confirm the WARNING fires correctly when a SkolePortal-only school still has |
|
Followed up in #355 with the small surfacing fix as discussed. Tagged you in the test-plan checklist for the SkolePortal-only-school-with-0001-still-listed verification. |
Summary
Some Danish schools license EasyIQ SkolePortal instead of the classic EasyIQ Ugeplan widget. These schools expose widget 0128 in Aula instead of the existing 0001. They look related (same vendor) but use a completely different SaaS backend —
skoleportal.easyiqcloud.dkvsapi.easyiqcloud.dk— with a different auth flow and event JSON shape.Without this PR, families with widget 0128 see the warning "You have enabled ugeplaner, but we cannot find any supported widgets (0029,0004,0001) in Aula" and no homework/weekplan data is fetched.
Refs #245.
What changed
ugeplan()— only runs when widget0128is detected, fully additive (no changes to existing 0001/0029/0004/0062 paths)._skoleportal_authenticate,_skoleportal_fetch_weekplan,_render_skoleportal_ugeplan, etc.) keep the new logic readable and testable.<details>/<summary>so each weekday is individually collapsible in HA's markdown card without any frontend dependencies. Today's section auto-expands in the current week.html.unescape()on text fields so Danish characters (æ,ø,å) render correctly instead ofæ/ø/å.How SkolePortal works (notes for reviewers)
widgetId=0128(existingget_tokenhelper)https://skoleportal.easyiqcloud.dk/Aula/AuthenticateAulaUserwithx-childfilter/x-institutionfilter/x-loginheaders → returns{loginId, child, childName, schoolName, schoolId}https://skoleportal.easyiqcloud.dk/Calendar/CalendarGetWeekplanEvents?loginId=...&date=...→ returns array of eventsStartTime(YYYY/MM/DD HH:MMDanish format),StartTimeISO,EndTime,CoursesDisplay(subject like "Matematik"),ActivitiesDisplay(class like "4D"),ChapterTitle,DescriptionTesting
Tested with a single child at a SkolePortal-licensed school (4. klasse, Skt. Klemensskolen). Multi-child path implemented but untested — feedback from anyone with multiple children at SkolePortal schools welcome.
The existing 0001/0029/0004/0062 paths are unchanged, so users without widget 0128 are unaffected.
Sample output
The
ugeplanattribute now contains:```html
Uge 18
Mandag
Matematik (4D)Tid: 08:00 - 08:45
Vi har arbejdet med decimaltal på s. 85.
Engelsk (4D)
Tid: 08:45 - 09:30
Eleverne er gået i gang med tegneserier på Pixton...
Tirsdag
...Renders nicely in a standard HA markdown card with browser-native expand/collapse per day.