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/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 88b16e3..2d44bdf 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 @@ -13,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"] @@ -36,32 +31,57 @@ 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) - return False # stop initialization on any exception + _LOGGER.error("INIT login failed: bad credentials. %s", e) + return False 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 data.""" + """Function to fetch grade and timetable 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.debug("grades_data: %s", grades_data) # Zeigt die Daten im Log - return 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)) + + # request all possible subjects + subjects_data = await edupage.get_subjects() +# _LOGGER.info("INIT subject count: " + str(len(subjects_data))) + + # request all possible students + students_data = await edupage.get_students() +# _LOGGER.info("INIT students count: " + str(len(students_data))) + + return { + "grades": grades_data, +# "timetable": timetable_data, + "user_id": userid, + "subjects": subjects_data + } + except Exception as e: - _LOGGER.error("error fetching grades data: %s", e) - return [] + _LOGGER.error("INIT error fetching data: %s", e) + return False coordinator = DataUpdateCoordinator( hass, @@ -71,22 +91,23 @@ 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.""" + _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..305dd1d 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 @@ -13,23 +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.debug("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) + return all_subjects + except Exception as e: + raise UpdateFailed(F"EDUPAGE error updating get_subjects() data from API: {e}") + + async def get_students(self): + + try: + all_students = await self.hass.async_add_executor_job(self.api.get_students) + return all_students + except Exception as e: + raise UpdateFailed(F"EDUPAGE error updating get_students() data from API: {e}") - async def get_timetable(self, dateTT: datetime): + async def get_user_id(self): try: - timetable = await self.hass.aync_add_executor_job(self.api.get_timetable()) - _LOGGER.debug("get_timetable() successful from API") - return timetable + 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 ac96345..6334a1e 100644 --- a/custom_components/homeassistantedupage/sensor.py +++ b/custom_components/homeassistantedupage/sensor.py @@ -1,48 +1,72 @@ +import logging from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant 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", []) - # 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) + # group grades based on subject_id + grades_by_subject = group_grades_by_subject(grades) + + 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 +