From f1ce09f845dcb4bc2def06fd38d2e29569cc789b Mon Sep 17 00:00:00 2001 From: spraot <10588348+spraot@users.noreply.github.com> Date: Mon, 11 May 2026 07:51:04 +0200 Subject: [PATCH] EasyIQ Ugeplan: port to skoleportal.easyiqcloud.dk and add widget 0128 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=&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

Uge NN

header. --- custom_components/aula/client.py | 302 +++++++++++++++++++------------ 1 file changed, 182 insertions(+), 120 deletions(-) diff --git a/custom_components/aula/client.py b/custom_components/aula/client.py index 5c2513a..b6f1b88 100644 --- a/custom_components/aula/client.py +++ b/custom_components/aula/client.py @@ -737,9 +737,10 @@ def mu_opgaver(week, thisnext): and "0004" not in self.widgets and "0062" not in self.widgets and "0001" not in self.widgets + and "0128" not in self.widgets ): _LOGGER.error( - "You have enabled ugeplaner, but we cannot find any supported widgets (0029,0004,0001) in Aula." + "You have enabled ugeplaner, but we cannot find any supported widgets (0029,0004,0001,0128) in Aula." ) if "0029" in self.widgets and "0004" in self.widgets: _LOGGER.warning( @@ -777,138 +778,199 @@ def ugeplan(week, thisnext): except: _LOGGER.debug("Cannot fetch ugeplaner, so setting as empty") _LOGGER.debug("ugeplaner response " + str(ugeplaner.text)) - if "0001" in self.widgets: - import calendar - - _LOGGER.debug("In the EasyIQ flow") - token = self.get_token("0001") - csrf_token = self._get_csrf_token() + easyiq_widget = next( + (w for w in ("0001", "0128") if w in self.widgets), None + ) + if easyiq_widget is not None: + _LOGGER.debug("In the EasyIQ flow (widget %s)", easyiq_widget) + token = self.get_token(easyiq_widget) + + # Monday 00:00 UTC of the requested ISO week, as JS-style + # ISO timestamp (the skoleportal calendar endpoint accepts + # any time on the target Monday — it derives the week + # itself). + iso_year, iso_week = (int(p) for p in week.split("-W")) + monday = datetime.date.fromisocalendar(iso_year, iso_week, 1) + week_iso = ( + datetime.datetime.combine( + monday, datetime.time(0, 0, 0), tzinfo=pytz.utc + ).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ) - easyiq_headers = { - "x-aula-institutionfilter": str(self._institutionProfiles[0]), - "x-aula-userprofile": "guardian", + skoleportal_common = { "Authorization": token, - "accept": "application/json", - "origin": "https://www.aula.dk", - "referer": "https://www.aula.dk/", - "authority": "api.easyiqcloud.dk", + "X-Login": guardian, + "X-InstitutionFilter": ",".join(self._institutionProfiles), + "X-UserProfile": "guardian", + "X-ChildFilter": ",".join(self._childuserids), + "X-Requested-With": "Fetch", + "Accept": "application/json", + "Origin": "https://skoleportal.easyiqcloud.dk", + "Referer": "https://skoleportal.easyiqcloud.dk/UgeplanWidget", + "User-Agent": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/147.0.0.0 Safari/537.36" + ), } - if csrf_token: - easyiq_headers["csrfp-token"] = csrf_token - - for child in self._childrenFirstNamesAndUserIDs.items(): - userid = child[0] - first_name = child[1] - - _LOGGER.debug("EasyIQ headers " + str(easyiq_headers)) - post_data = { - "sessionId": guardian, - "currentWeekNr": week, - "userProfile": "guardian", - "institutionFilter": self._institutionProfiles, - "childFilter": [userid], - } - _LOGGER.debug("EasyIQ post data " + str(post_data)) - ugeplaner = requests.post( - EASYIQ_API + "/weekplaninfo", - json=post_data, - headers=easyiq_headers, - verify=True, - ) - # _LOGGER.debug( - # "EasyIQ Opgaver status_code " + str(ugeplaner.status_code) - # ) - _LOGGER.debug( - "EasyIQ Opgaver response " + str(ugeplaner.json()) - ) - _ugep = ( - "

" - # + ugeplaner.json()["Weekplan"]["ActivityName"] - + " Uge " - + week.split("-W")[1] - # + ugeplaner.json()["Weekplan"]["WeekNo"] - + "

" - ) - # from datetime import datetime - - def findDay(date): - day, month, year = (int(i) for i in date.split(" ")) - dayNumber = calendar.weekday(year, month, day) - days = [ - "Mandag", - "Tirsdag", - "Onsdag", - "Torsdag", - "Fredag", - "Lørdag", - "Søndag", - ] - return days[dayNumber] - def is_correct_format(date_string, format): + danish_days = [ + "Mandag", + "Tirsdag", + "Onsdag", + "Torsdag", + "Fredag", + "Lørdag", + "Søndag", + ] + + for userid, first_name in self._childrenFirstNamesAndUserIDs.items(): + child_headers = dict(skoleportal_common) + child_headers["X-Child"] = userid + + try: + auth_resp = requests.post( + "https://skoleportal.easyiqcloud.dk/Aula/AuthenticateAulaUser", + headers={**child_headers, "Content-Length": "0"}, + data=b"", + timeout=15, + ) + except requests.RequestException as exc: + _LOGGER.warning( + "EasyIQ AuthenticateAulaUser failed for %s: %s", + first_name, + exc, + ) + continue + if auth_resp.status_code != 200: + _LOGGER.warning( + "EasyIQ AuthenticateAulaUser %s for %s: %s", + auth_resp.status_code, + first_name, + auth_resp.text[:200], + ) + continue + try: + login_id = auth_resp.json().get("loginId") + except ValueError: + _LOGGER.warning( + "EasyIQ AuthenticateAulaUser returned non-JSON for %s: %s", + first_name, + auth_resp.text[:200], + ) + continue + if not login_id: + _LOGGER.warning( + "EasyIQ AuthenticateAulaUser missing loginId for %s: %s", + first_name, + auth_resp.text[:200], + ) + continue + + try: + events_resp = requests.get( + "https://skoleportal.easyiqcloud.dk/Calendar/CalendarGetWeekplanEvents", + params={ + "loginId": login_id, + "date": week_iso, + "activityFilter": "-1", + "courseFilter": "-1", + "textFilter": "", + "ownWeekPlan": "false", + }, + headers=child_headers, + timeout=15, + ) + except requests.RequestException as exc: + _LOGGER.warning( + "EasyIQ CalendarGetWeekplanEvents failed for %s week %s: %s", + first_name, + week, + exc, + ) + continue + if events_resp.status_code != 200: + _LOGGER.warning( + "EasyIQ CalendarGetWeekplanEvents %s for %s week %s: %s", + events_resp.status_code, + first_name, + week, + events_resp.text[:200], + ) + continue + try: + events = events_resp.json() + except ValueError: + _LOGGER.warning( + "EasyIQ CalendarGetWeekplanEvents non-JSON for %s: %s", + first_name, + events_resp.text[:200], + ) + continue + if not isinstance(events, list): + _LOGGER.warning( + "EasyIQ CalendarGetWeekplanEvents unexpected shape for %s: %s", + first_name, + type(events).__name__, + ) + events = [] + + _ugep = "

Uge " + week.split("-W")[1] + "

" + events.sort(key=lambda e: e.get("StartTime") or "") + for ev in events: + start_str = ev.get("StartTime") + end_str = ev.get("EndTime") + if not start_str or not end_str: + continue try: - datetime.datetime.strptime(date_string, format) - return True + start_dt = datetime.datetime.strptime( + start_str, "%Y/%m/%d %H:%M" + ) + end_dt = datetime.datetime.strptime( + end_str, "%Y/%m/%d %H:%M" + ) except ValueError: _LOGGER.debug( - "Could not parse timestamp: " + str(date_string) + "EasyIQ event for %s: unrecognised time format %r/%r", + first_name, + start_str, + end_str, ) - return False - - try: - for i in ugeplaner.json()["Events"]: - if is_correct_format(i["start"], "%Y/%m/%d %H:%M"): - _LOGGER.debug("No Event") - start_datetime = datetime.datetime.strptime( - i["start"], "%Y/%m/%d %H:%M" - ) - _LOGGER.debug(start_datetime) - end_datetime = datetime.datetime.strptime( - i["end"], "%Y/%m/%d %H:%M" - ) - if start_datetime.date() == end_datetime.date(): - formatted_day = findDay( - start_datetime.strftime("%d %m %Y") - ) - formatted_start = start_datetime.strftime( - " %H:%M" - ) - formatted_end = end_datetime.strftime("- %H:%M") - dresult = f"{formatted_day} {formatted_start} {formatted_end}" - else: - formatted_start = findDay( - start_datetime.strftime("%d %m %Y") - ) - formatted_end = findDay( - end_datetime.strftime("%d %m %Y") - ) - dresult = f"{formatted_start} {formatted_end}" - _ugep = _ugep + "
" + dresult + "
" - if i["itemType"] == "5": - _ugep = ( - _ugep - + "
" - + str(i["title"]) - + "
" - ) - else: - _ugep = ( - _ugep - + "
" - + str(i["ownername"]) - + "
" - ) - _ugep = _ugep + str(i["description"]) + "
" - else: - _LOGGER.debug("None") - except KeyError: - _LOGGER.debug("None") + continue + if start_dt.date() == end_dt.date(): + dresult = ( + f"{danish_days[start_dt.weekday()]} " + f"{start_dt.strftime('%H:%M')}" + f"- {end_dt.strftime('%H:%M')}" + ) + else: + dresult = ( + f"{danish_days[start_dt.weekday()]} " + f"{danish_days[end_dt.weekday()]}" + ) + _ugep += "
" + dresult + "
" + subject = ( + ev.get("CoursesDisplay") + or ev.get("Title") + or "" + ).strip() + if subject: + _ugep += "
" + str(subject) + "
" + description = ev.get("Description") or "" + if description: + _ugep += str(description) + "
" if thisnext == "this": self.ugep_attr[first_name] = _ugep elif thisnext == "next": self.ugepnext_attr[first_name] = _ugep - _LOGGER.debug("EasyIQ result: " + str(_ugep)) + _LOGGER.debug( + "EasyIQ result for %s week %s (%d events): %s", + first_name, + week, + len(events), + _ugep[:200], + ) if "0062" in self.widgets: _LOGGER.debug("In the Huskelisten flow...")