From c101e60f83a9d97f67a123d219382ad164914c33 Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Thu, 7 Nov 2024 13:15:51 +0100 Subject: [PATCH 1/3] work in progress api bug --- .gitignore | 5 ++- .../homeassistantedupage/__init__.py | 36 +++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 3df573a..f8d5e39 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,7 @@ cython_debug/ #.idea/ # secrets -*creds.sec* \ No newline at end of file +*creds.sec* + +# test +test/ \ No newline at end of file diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index 88b16e3..da87a9e 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -1,6 +1,7 @@ import logging import asyncio from datetime import timedelta +import datetime from edupage_api.exceptions import BadCredentialsException, CaptchaException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -52,16 +53,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: fetch_lock = asyncio.Lock() async def fetch_data(): - """function to fetch grade data.""" + """Function to fetch grade and timetable data.""" async with fetch_lock: - try: + # Abrufen der Noten grades_data = await edupage.get_grades() - _LOGGER.debug("grades_data: %s", grades_data) # Zeigt die Daten im Log - return grades_data + # _LOGGER.debug("grades_data: %s", grades_data) # Zeigt die Noten im Log + + # Abrufen des Stundenplans mit dem aktuellen Datum + current_date = datetime.date(2024, 11, 6) # Beispielformat: "2024-11-06" + timetable_data = await edupage.get_my_timetable(current_date) + + _LOGGER.debug("timetable_data: %s", timetable_data) # Zeigt den Stundenplan im Log + + # Kombiniere Noten und Stundenplan in einem Dictionary + return { + "grades": grades_data, + "timetable": timetable_data + } + except Exception as e: - _LOGGER.error("error fetching grades data: %s", e) - return [] + _LOGGER.error("error fetching data: %s", e) + return { + "grades": [], + "timetable": [] + } coordinator = DataUpdateCoordinator( hass, @@ -71,20 +87,20 @@ async def fetch_data(): update_interval=timedelta(minutes=5), ) - # first data fetch + # First data fetch await asyncio.sleep(1) await coordinator.async_config_entry_first_refresh() - #await coordinator.async_request_refresh() - # save coordinator + # Save coordinator hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - # platforms forward + # Forward platforms await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR]) return True + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload ConfigEntry.""" unload_ok = await hass.config_entries.async_forward_entry_unload(entry, Platform.SENSOR) From 07f17447c88c99e4bb3ecfc2263dbc663ca200d3 Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Fri, 22 Nov 2024 09:25:35 +0100 Subject: [PATCH 2/3] overhaul part 1 --- .../homeassistantedupage/__init__.py | 41 +++++++++++-------- .../homeassistant_edupage.py | 29 ++++++++++--- .../homeassistantedupage/sensor.py | 4 ++ 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index da87a9e..d49e3de 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -14,19 +14,13 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: """only ConfigEntry supported, no configuration.yaml yet""" + _LOGGER.info("INIT called async_setup") return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """create ConfigEntry an DataUpdateCoordinator""" - _LOGGER.info("called async_setup_entry") - - username = entry.data["username"] - password = entry.data["password"] - subdomain = entry.data["subdomain"] - edupage = Edupage(hass) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """initializin EduPage-integration and validate API-login""" + _LOGGER.info("INIT called async_setup_entry") + username = entry.data["username"] password = entry.data["password"] subdomain = entry.data["subdomain"] @@ -37,43 +31,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: login_success = await hass.async_add_executor_job( edupage.login, username, password, subdomain ) + _LOGGER.info("INIT login_success") except BadCredentialsException as e: - _LOGGER.error("login failed: bad credentials. %s", e) + _LOGGER.error("INIT login failed: bad credentials. %s", e) return False # stop initialization on any exception except CaptchaException as e: - _LOGGER.error("login failed: CAPTCHA needed. %s", e) + _LOGGER.error("INIT login failed: CAPTCHA needed. %s", e) return False except Exception as e: - _LOGGER.error("unexpected login error: %s", e) + _LOGGER.error("INIT unexpected login error: %s", e) return False fetch_lock = asyncio.Lock() async def fetch_data(): """Function to fetch grade and timetable data.""" + _LOGGER.info("INIT called fetch_data") async with fetch_lock: try: - # Abrufen der Noten + # request grades grades_data = await edupage.get_grades() - # _LOGGER.debug("grades_data: %s", grades_data) # Zeigt die Noten im Log + _LOGGER.info("INIT grade count: " + str(len(grades_data))) + + # request user_id + userid = await edupage.get_user_id() + _LOGGER.info("INIT user_id"+str(userid)) + + # request all possible subjects + subjects_data = await edupage.get_subjects() + _LOGGER.info("INIT subject count: " + str(len(subjects_data))) # Abrufen des Stundenplans mit dem aktuellen Datum current_date = datetime.date(2024, 11, 6) # Beispielformat: "2024-11-06" - timetable_data = await edupage.get_my_timetable(current_date) + timetable_data = await edupage.get_timetable(current_date) _LOGGER.debug("timetable_data: %s", timetable_data) # Zeigt den Stundenplan im Log # Kombiniere Noten und Stundenplan in einem Dictionary return { "grades": grades_data, - "timetable": timetable_data + "timetable": timetable_data, + "user_id": userid, + "subjects": subjects_data } except Exception as e: - _LOGGER.error("error fetching data: %s", e) + _LOGGER.error("INIT error fetching data: %s", e) return { "grades": [], "timetable": [] @@ -103,6 +109,7 @@ async def fetch_data(): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload ConfigEntry.""" + _LOGGER.info("INIT called async_unload_entry") unload_ok = await hass.config_entries.async_forward_entry_unload(entry, Platform.SENSOR) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/custom_components/homeassistantedupage/homeassistant_edupage.py b/custom_components/homeassistantedupage/homeassistant_edupage.py index 5c9e9dc..bee090b 100644 --- a/custom_components/homeassistantedupage/homeassistant_edupage.py +++ b/custom_components/homeassistantedupage/homeassistant_edupage.py @@ -4,6 +4,7 @@ from homeassistant.helpers.update_coordinator import UpdateFailed _LOGGER = logging.getLogger(__name__) + class Edupage: def __init__(self,hass): self.hass = hass @@ -17,17 +18,35 @@ async def get_grades(self): try: grades = await self.hass.async_add_executor_job(self.api.get_grades) - _LOGGER.debug("get_grades() successful from API") + _LOGGER.info("API get_grades() successful from API") return grades except Exception as e: raise UpdateFailed(F"error updating get_grades() data from API: {e}") - async def get_timetable(self, dateTT: datetime): + async def get_subjects(self): + + try: + all_subjects = await self.hass.async_add_executor_job(self.api.get_subjects) + _LOGGER.info("API get_subjects") + return all_subjects + except Exception as e: + raise UpdateFailed(F"error updating get_subjects() data from API: {e}") + + async def get_user_id(self): + + try: + user_id_data = await self.hass.async_add_executor_job(self.api.get_user_id) + _LOGGER.info("API get_user_id") + return user_id_data + except Exception as e: + raise UpdateFailed(F"error updating get_user_id() data from API: {e}") + + async def get_timetable(self): try: - timetable = await self.hass.aync_add_executor_job(self.api.get_timetable()) - _LOGGER.debug("get_timetable() successful from API") - return timetable + timetable_data = await self.hass.async_add_executor_job(self.api.get_timetable) + _LOGGER.info("API get_timetable") + return timetable_data except Exception as e: raise UpdateFailed(F"error updating get_timetable() data from API: {e}") diff --git a/custom_components/homeassistantedupage/sensor.py b/custom_components/homeassistantedupage/sensor.py index ac96345..c53d765 100644 --- a/custom_components/homeassistantedupage/sensor.py +++ b/custom_components/homeassistantedupage/sensor.py @@ -1,3 +1,4 @@ +import logging from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -5,8 +6,11 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +_LOGGER = logging.getLogger("custom_components.homeassistant_edupage") + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None: """Set up EduPage sensors based on subjects in the data.""" + _LOGGER.info("SENSOR called async_setup_entry") coordinator = hass.data[DOMAIN][entry.entry_id] subjects = {} From 184e5670a19289cd33d72022d532f0c043611d1a Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Fri, 22 Nov 2024 12:56:50 +0100 Subject: [PATCH 3/3] overhauled sensor to get any subject no matter if grades are not --- README.md | 16 +++--- .../homeassistantedupage/__init__.py | 28 +++++----- .../homeassistant_edupage.py | 32 ++++++----- .../homeassistantedupage/sensor.py | 54 +++++++++++++------ 4 files changed, 74 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index a7bae60..ab4cea7 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ # homeassistantedupage An HomeAssistant integration of the EduPage Schooling System based on the edupage_api library found here https://github.com/EdupageAPI/edupage-api -## Installation without HACS -* Extract files in /custom_components/homeassistantedupage to your installation. -* restart Home Assistant -* Add new integration and search for "Edupage" -* enter Username, Password and Subdomain (w/o ".edupage.org") -* based on your subjects you should find more or less sensors now, named bei the subject with grade-counts -* data is to be found as "attributes", see screenshot +# IMPORTANT +In this phase of development please remove integration after update and reinstall because there are major changes. ## Installation with HACS * open HACS @@ -17,13 +12,14 @@ An HomeAssistant integration of the EduPage Schooling System based on the edupag * type "integration" * add * choose download -* please alway select at least a release with "HACS" in releasename +* please alway select the last one because its work in progress * restart HA * add integration -* look for "edupage" +* look for "edupage" with the nice "E" icon * use "homeassistantedupage" integration * enter login, password und (ONLY!) subdomain (no .edupage.com or something) -* if there are grades in your account there should spawn one ore more entities +* you should see now a lot of sensors with the subjects of your school +* grades are attributes if existing ![screenshot of sensors](./img/edupage_subjects_grades.jpg) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index d49e3de..2d44bdf 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except BadCredentialsException as e: _LOGGER.error("INIT login failed: bad credentials. %s", e) - return False # stop initialization on any exception + return False except CaptchaException as e: _LOGGER.error("INIT login failed: CAPTCHA needed. %s", e) @@ -52,38 +52,36 @@ async def fetch_data(): _LOGGER.info("INIT called fetch_data") async with fetch_lock: try: + # request classes + classes_data = await edupage.get_classes() +# _LOGGER.info("INIT classes count: " + str(len(classes_data))) + # request grades grades_data = await edupage.get_grades() - _LOGGER.info("INIT grade count: " + str(len(grades_data))) +# _LOGGER.info("INIT grade count: " + str(len(grades_data))) # request user_id userid = await edupage.get_user_id() - _LOGGER.info("INIT user_id"+str(userid)) +# _LOGGER.info("INIT user_id: "+str(userid)) # request all possible subjects subjects_data = await edupage.get_subjects() - _LOGGER.info("INIT subject count: " + str(len(subjects_data))) +# _LOGGER.info("INIT subject count: " + str(len(subjects_data))) - # Abrufen des Stundenplans mit dem aktuellen Datum - current_date = datetime.date(2024, 11, 6) # Beispielformat: "2024-11-06" - timetable_data = await edupage.get_timetable(current_date) + # request all possible students + students_data = await edupage.get_students() +# _LOGGER.info("INIT students count: " + str(len(students_data))) - _LOGGER.debug("timetable_data: %s", timetable_data) # Zeigt den Stundenplan im Log - - # Kombiniere Noten und Stundenplan in einem Dictionary return { "grades": grades_data, - "timetable": timetable_data, +# "timetable": timetable_data, "user_id": userid, "subjects": subjects_data } except Exception as e: _LOGGER.error("INIT error fetching data: %s", e) - return { - "grades": [], - "timetable": [] - } + return False coordinator = DataUpdateCoordinator( hass, diff --git a/custom_components/homeassistantedupage/homeassistant_edupage.py b/custom_components/homeassistantedupage/homeassistant_edupage.py index bee090b..305dd1d 100644 --- a/custom_components/homeassistantedupage/homeassistant_edupage.py +++ b/custom_components/homeassistantedupage/homeassistant_edupage.py @@ -14,41 +14,45 @@ def login(self, username, password, subdomain): return self.api.login(username, password, subdomain) + async def get_classes(self): + + try: + classes_data = await self.hass.async_add_executor_job(self.api.get_classes) + return classes_data + except Exception as e: + raise UpdateFailed(F"EDUPAGE error updating get_classes() data from API: {e}") + async def get_grades(self): try: grades = await self.hass.async_add_executor_job(self.api.get_grades) - _LOGGER.info("API get_grades() successful from API") return grades except Exception as e: - raise UpdateFailed(F"error updating get_grades() data from API: {e}") + raise UpdateFailed(F"EDUPAGE error updating get_grades() data from API: {e}") async def get_subjects(self): try: all_subjects = await self.hass.async_add_executor_job(self.api.get_subjects) - _LOGGER.info("API get_subjects") return all_subjects except Exception as e: - raise UpdateFailed(F"error updating get_subjects() data from API: {e}") + raise UpdateFailed(F"EDUPAGE error updating get_subjects() data from API: {e}") - async def get_user_id(self): + async def get_students(self): try: - user_id_data = await self.hass.async_add_executor_job(self.api.get_user_id) - _LOGGER.info("API get_user_id") - return user_id_data + all_students = await self.hass.async_add_executor_job(self.api.get_students) + return all_students except Exception as e: - raise UpdateFailed(F"error updating get_user_id() data from API: {e}") + raise UpdateFailed(F"EDUPAGE error updating get_students() data from API: {e}") - async def get_timetable(self): + async def get_user_id(self): try: - timetable_data = await self.hass.async_add_executor_job(self.api.get_timetable) - _LOGGER.info("API get_timetable") - return timetable_data + user_id_data = await self.hass.async_add_executor_job(self.api.get_user_id) + return user_id_data except Exception as e: - raise UpdateFailed(F"error updating get_timetable() data from API: {e}") + raise UpdateFailed(F"EDUPAGE error updating get_user_id() data from API: {e}") async def async_update(self): diff --git a/custom_components/homeassistantedupage/sensor.py b/custom_components/homeassistantedupage/sensor.py index c53d765..6334a1e 100644 --- a/custom_components/homeassistantedupage/sensor.py +++ b/custom_components/homeassistantedupage/sensor.py @@ -5,48 +5,68 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from collections import defaultdict _LOGGER = logging.getLogger("custom_components.homeassistant_edupage") +def group_grades_by_subject(grades): + """grouping grades based on subject_id.""" + grouped = defaultdict(list) + for grade in grades: + grouped[grade.subject_id].append(grade) + return grouped + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None: - """Set up EduPage sensors based on subjects in the data.""" + """Set up EduPage sensors based on subjects and their grades.""" _LOGGER.info("SENSOR called async_setup_entry") + coordinator = hass.data[DOMAIN][entry.entry_id] - subjects = {} + subjects = coordinator.data.get("subjects", []) + grades = coordinator.data.get("grades", []) + + # group grades based on subject_id + grades_by_subject = group_grades_by_subject(grades) - # Sortiere Noten nach Fächern - for grade in coordinator.data: - subject = grade.subject_name - if subject not in subjects: - subjects[subject] = [] - subjects[subject].append(grade) + sensors = [] + for subject in subjects: + # get grades per subject based on subject_id + subject_grades = grades_by_subject.get(subject.subject_id, []) + sensor = EduPageSubjectSensor( + coordinator, + subject.name, + subject_grades + ) + sensors.append(sensor) - # Erstelle für jedes Fach einen Sensor - sensors = [EduPageSubjectSensor(coordinator, subject, grades) for subject, grades in subjects.items()] async_add_entities(sensors, True) class EduPageSubjectSensor(CoordinatorEntity, SensorEntity): - """Sensor-Entität für ein bestimmtes Unterrichtsfach.""" + """subject sensor entity.""" - def __init__(self, coordinator, subject_name, grades): - """Initialisierung des Fach-Sensors.""" + def __init__(self, coordinator, subject_name, grades=None): + """initializing""" super().__init__(coordinator) self._subject_name = subject_name - self._grades = grades - self._attr_name = f"EduPage Noten - {subject_name}" # Name des Sensors basierend auf dem Fach + self._grades = grades or [] + self._attr_name = f"EduPage subject - {subject_name}" self._attr_unique_id = f"edupage_grades_{subject_name.lower().replace(' ', '_')}" @property def state(self): - """Gibt die Anzahl der Noten für dieses Fach zurück.""" + """return grade count""" return len(self._grades) @property def extra_state_attributes(self): - """Rückgabe zusätzlicher Attribute für den Sensor.""" + """return additional attributes""" + if not self._grades: + return {"info": "no grades yet"} + attributes = {} for i, grade in enumerate(self._grades): attributes[f"grade_{i+1}_title"] = grade.title attributes[f"grade_{i+1}_grade_n"] = grade.grade_n attributes[f"grade_{i+1}_date"] = grade.date.strftime("%Y-%m-%d %H:%M:%S") + attributes[f"grade_{i+1}_teacher"] = grade.teacher.name return attributes +