From 0e26237a51116a821fe4e45bf57085b4047983dc Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Fri, 22 Nov 2024 15:30:43 +0100 Subject: [PATCH 1/7] start adding timetable support --- .../homeassistantedupage/__init__.py | 30 ++++-- .../homeassistantedupage/calendar.py | 91 +++++++++++-------- .../homeassistant_edupage.py | 77 ++++++++++++++++ .../homeassistantedupage/manifest.json | 1 + 4 files changed, 155 insertions(+), 44 deletions(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index 2d44bdf..e3f5a1b 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -54,27 +54,43 @@ async def fetch_data(): try: # request classes classes_data = await edupage.get_classes() -# _LOGGER.info("INIT classes count: " + str(len(classes_data))) + _LOGGER.info("INIT classes fount: " + str(len(classes_data))) + # _LOGGER.info("INIT classes %s", classes_data) # request grades grades_data = await edupage.get_grades() -# _LOGGER.info("INIT grade count: " + str(len(grades_data))) + _LOGGER.info("INIT grades fount: " + 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 subjects fount: " + str(len(subjects_data))) # request all possible students students_data = await edupage.get_students() -# _LOGGER.info("INIT students count: " + str(len(students_data))) + _LOGGER.info("INIT students fount: " + str(len(students_data))) + + # request all the teachers + teachers_data = await edupage.get_teachers() + _LOGGER.info("INIT teachers found " + str(len(teachers_data))) + + # request all the classrooms + classrooms_data = await edupage.get_classrooms() + _LOGGER.info("INIT classrooms found: " + str(len(classrooms_data))) + + # request timetable + timetable_data = await edupage.get_timetable() + if timetable_data is None: + _LOGGER.info("INIT timettable_data is None") + else: + _LOGGER.info("INIT lessons found: %s", str(len(timetable_data.lessons))) return { "grades": grades_data, -# "timetable": timetable_data, + "timetable": timetable_data, "user_id": userid, "subjects": subjects_data } @@ -100,7 +116,7 @@ async def fetch_data(): hass.data[DOMAIN][entry.entry_id] = coordinator # Forward platforms - await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR]) + await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR, Platform.CALENDAR]) return True diff --git a/custom_components/homeassistantedupage/calendar.py b/custom_components/homeassistantedupage/calendar.py index 3dd51b6..3b1f87b 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -1,48 +1,65 @@ -import voluptuous as vol -import logging +from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.util import dt as dt_util from datetime import datetime, timedelta -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.core import HomeAssistant -from .homeassistant_edupage import Edupage -from homeassistant.components.calendar import CalendarEntity +# Importiere die EduPage-API-Klassen, falls nötig +# from edupage_api.models import Timetable, Lesson -_LOGGER = logging.getLogger(__name__) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the EduPage Calendar platform.""" + if discovery_info is None: + return -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback): + # Füge eine Instanz des Kalenders hinzu + async_add_entities([EduPageCalendar(hass, discovery_info["name"], discovery_info["timetable_data"])]) - username = entry.data["username"] - password = entry.data["password"] - subdomain = entry.data["subdomain"] - edupage = Edupage(hass) - unique_id = f"edupage_{username}_calendar" - await hass.async_add_executor_job(edupage.login, username, password, subdomain) +class EduPageCalendar(CalendarEventDevice): + """Representation of the EduPage Calendar.""" - async def async_update_data(): + def __init__(self, hass, name, timetable_data): + """Initialize the calendar.""" + self._hass = hass + self._name = name + self._timetable_data = timetable_data + self._events = [] - today = datetime.now().date() - try: - return await edupage.get_timetable(today) - except Exception as e: - _LOGGER.error(f"error updating data: {e}") - raise UpdateFailed(F"error updating data: {e}") - - async_add_entities([TimetableCalendar(edupage, unique_id)], True) - -class TimetableCalendar(CalendarEntity): - def __init__(self, edupage, unique_id): - self.edupage = edupage - self._attr_unique_id = unique_id - @property def name(self): - """return name of calendar""" + """Return the name of the calendar.""" return self._name - - async def async_get_events(self, today: datetime): - timetable = self.get_timetable(today) - # Konvertieren Sie 'timetable' in eine Liste von Ereignissen, die von dieser Methode zurückgegeben werden - return timetable #dict + + async def async_get_events(self, hass, start_date, end_date): + """Return all events in the specified date range.""" + events = [] + for lesson in self._timetable_data.lessons: + start_time = datetime.combine(start_date, lesson.start_time) + end_time = datetime.combine(start_date, lesson.end_time) + + # Füge das Event hinzu, wenn es im Zeitfenster liegt + if start_time >= start_date and end_time <= end_date: + event = { + "title": lesson.subject.name, + "start": start_time.isoformat(), + "end": end_time.isoformat(), + "description": f"Lehrer: {', '.join([t.name for t in lesson.teachers])}", + } + events.append(event) + + self._events = events + return events + + @property + def extra_state_attributes(self): + """Extra attributes of the calendar.""" + return {"number_of_events": len(self._events)} + + @property + def event(self): + """Return the next upcoming event.""" + now = dt_util.now() + for event in self._events: + if datetime.fromisoformat(event["start"]) > now: + return event + return None diff --git a/custom_components/homeassistantedupage/homeassistant_edupage.py b/custom_components/homeassistantedupage/homeassistant_edupage.py index 305dd1d..6b0a0ac 100644 --- a/custom_components/homeassistantedupage/homeassistant_edupage.py +++ b/custom_components/homeassistantedupage/homeassistant_edupage.py @@ -1,7 +1,14 @@ import logging from edupage_api import Edupage as APIEdupage +from edupage_api.classes import Class +from edupage_api.people import EduTeacher +from edupage_api.people import Gender +from edupage_api.classrooms import Classroom + from datetime import datetime +from datetime import date from homeassistant.helpers.update_coordinator import UpdateFailed +from concurrent.futures import ThreadPoolExecutor _LOGGER = logging.getLogger(__name__) @@ -54,6 +61,76 @@ async def get_user_id(self): except Exception as e: raise UpdateFailed(F"EDUPAGE error updating get_user_id() data from API: {e}") + async def get_classrooms(self): + + try: + all_classrooms = await self.hass.async_add_executor_job(self.api.get_classrooms) + return all_classrooms + except Exception as e: + raise UpdateFailed(F"EDUPAGE error updating get_classrooms data from API: {e}") + + async def get_teachers(self): + + try: + all_teachers = await self.hass.async_add_executor_job(self.api.get_teachers) + return all_teachers + except Exception as e: + raise UpdateFailed(F"EDUPAGE error updating get_teachers data from API: {e}") + + async def get_timetable(self): + try: + _LOGGER.debug("Begin creating first teacher instance") + teacher1 = EduTeacher( + person_id=-17, + name="Anka Kehr", + gender=Gender.FEMALE, + in_school_since=None, + classroom_name="Haus 1 R 08", + teacher_to=None + ) + _LOGGER.debug("First teacher instance created: %s", teacher1) + + teacher2 = EduTeacher( + person_id=-25, + name="Christiane Koch", + gender=Gender.FEMALE, + in_school_since=None, + classroom_name="Haus 1 R 08", + teacher_to=None + ) + _LOGGER.debug("Teacher2 created successfully: %s", teacher2) + + classroom = Classroom( + classroom_id=-12, + name="Haus 1 R 08", + short="H1 R08" + ) + _LOGGER.debug("Classroom created successfully: %s", classroom) + + class_instance = Class( + class_id=-28, + name="4b", + short="4b", + homeroom_teachers=[teacher1, teacher2], + homeroom=classroom, + grade=None + ) + _LOGGER.debug("Class instance created successfully: %s", class_instance) + + except Exception as e: + _LOGGER.error("Error during instantiation: %s", e) + + try: + executor = ThreadPoolExecutor(max_workers=5) + timetable_data = await self.hass.async_add_executor_job(self.api.get_timetable, class_instance, date.today()) + if timetable_data is None: + _LOGGER.info("EDUPAGE timetable is None") + else: + _LOGGER.info("EDUPAGE timetable_data: $s", timetable_data) + return timetable_data + except Exception as e: + raise UpdateFailed(F"EDUPAGE error updating get_timetable() data from API: {e}") + async def async_update(self): pass diff --git a/custom_components/homeassistantedupage/manifest.json b/custom_components/homeassistantedupage/manifest.json index 7ca50f3..b8c1d7a 100644 --- a/custom_components/homeassistantedupage/manifest.json +++ b/custom_components/homeassistantedupage/manifest.json @@ -8,5 +8,6 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/rine77/homeassistantedupage/issues", "requirements": ["edupage_api==0.11.0"], + "supported_features": ["sensor", "calendar"], "version": "0.1.0" } \ No newline at end of file From bb34cb91341c4dad26aabe437f8214cd7c83d3c0 Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Fri, 22 Nov 2024 17:23:14 +0100 Subject: [PATCH 2/7] corrected manifest.json to success HACS action --- custom_components/homeassistantedupage/__init__.py | 2 ++ custom_components/homeassistantedupage/manifest.json | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index e3f5a1b..ec0dac4 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -10,6 +10,8 @@ from .homeassistant_edupage import Edupage from .const import DOMAIN +PLATFORMS = ["sensor", "calendar"] + _LOGGER = logging.getLogger("custom_components.homeassistant_edupage") async def async_setup(hass: HomeAssistant, config: dict) -> bool: diff --git a/custom_components/homeassistantedupage/manifest.json b/custom_components/homeassistantedupage/manifest.json index b8c1d7a..df91a3e 100644 --- a/custom_components/homeassistantedupage/manifest.json +++ b/custom_components/homeassistantedupage/manifest.json @@ -8,6 +8,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/rine77/homeassistantedupage/issues", "requirements": ["edupage_api==0.11.0"], - "supported_features": ["sensor", "calendar"], - "version": "0.1.0" + "version": "0.1.4" } \ No newline at end of file From 13df38305346a4d2e2c08130a44a4a21209ae199 Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Sat, 23 Nov 2024 11:18:43 +0100 Subject: [PATCH 3/7] timetable calendar working with static class_instance --- .../homeassistantedupage/__init__.py | 128 +++++++++----- .../homeassistantedupage/calendar.py | 163 ++++++++++++------ .../homeassistant-edupage | 1 - .../homeassistant_edupage.py | 48 +----- 4 files changed, 202 insertions(+), 138 deletions(-) delete mode 120000 custom_components/homeassistantedupage/homeassistant-edupage diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index ec0dac4..9f34042 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -1,17 +1,18 @@ import logging import asyncio -from datetime import timedelta -import datetime +from datetime import datetime, timedelta from edupage_api.exceptions import BadCredentialsException, CaptchaException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.const import Platform from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .homeassistant_edupage import Edupage +from edupage_api.classes import Class +from edupage_api.people import EduTeacher +from edupage_api.people import Gender +from edupage_api.classrooms import Classroom from .const import DOMAIN -PLATFORMS = ["sensor", "calendar"] - _LOGGER = logging.getLogger("custom_components.homeassistant_edupage") async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -27,7 +28,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: password = entry.data["password"] subdomain = entry.data["subdomain"] edupage = Edupage(hass) - unique_id_sensorGrade = f"edupage_{username}_gradesensor" + + try: + # _LOGGER.debug("Begin creating first teacher instance") + teacher1 = EduTeacher( + person_id=-17, + name="Anka Kehr", + gender=Gender.FEMALE, + in_school_since=None, + classroom_name="Haus 1 R 08", + teacher_to=None + ) + # _LOGGER.debug("First teacher instance created: %s", teacher1) + + teacher2 = EduTeacher( + person_id=-25, + name="Christiane Koch", + gender=Gender.FEMALE, + in_school_since=None, + classroom_name="Haus 1 R 08", + teacher_to=None + ) + # _LOGGER.debug("Teacher2 created successfully: %s", teacher2) + + classroom = Classroom( + classroom_id=-12, + name="Haus 1 R 08", + short="H1 R08" + ) + # _LOGGER.debug("Classroom created successfully: %s", classroom) + + class_instance = Class( + class_id=-28, + name="4b", + short="4b", + homeroom_teachers=[teacher1, teacher2], + homeroom=classroom, + grade=None + ) + # _LOGGER.debug("Class instance created successfully: %s", class_instance) + + except Exception as e: + _LOGGER.error("INIT Error during instantiation: %s", e) try: login_success = await hass.async_add_executor_job( @@ -52,62 +94,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def fetch_data(): """Function to fetch grade and timetable data.""" _LOGGER.info("INIT called fetch_data") + async with fetch_lock: try: - # request classes + # Daten abrufen classes_data = await edupage.get_classes() - _LOGGER.info("INIT classes fount: " + str(len(classes_data))) - # _LOGGER.info("INIT classes %s", classes_data) - - # request grades grades_data = await edupage.get_grades() - _LOGGER.info("INIT grades fount: " + 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 subjects fount: " + str(len(subjects_data))) - - # request all possible students students_data = await edupage.get_students() - _LOGGER.info("INIT students fount: " + str(len(students_data))) - - # request all the teachers teachers_data = await edupage.get_teachers() - _LOGGER.info("INIT teachers found " + str(len(teachers_data))) - - # request all the classrooms classrooms_data = await edupage.get_classrooms() - _LOGGER.info("INIT classrooms found: " + str(len(classrooms_data))) - # request timetable - timetable_data = await edupage.get_timetable() - if timetable_data is None: - _LOGGER.info("INIT timettable_data is None") - else: - _LOGGER.info("INIT lessons found: %s", str(len(timetable_data.lessons))) + # Stundenplan für die nächsten 14 Tage abrufen + today = datetime.now().date() + timetable_data = {} + for offset in range(14): + current_date = today + timedelta(days=offset) + timetable_data[current_date] = await edupage.get_timetable(class_instance, current_date) + + _LOGGER.info("INIT got timetables before return") + # Daten zusammenstellen return { + "classes": classes_data, "grades": grades_data, - "timetable": timetable_data, "user_id": userid, - "subjects": subjects_data + "subjects": subjects_data, + "students": students_data, + "teachers": teachers_data, + "classrooms": classrooms_data, + "timetable": timetable_data, } except Exception as e: - _LOGGER.error("INIT error fetching data: %s", e) + _LOGGER.error("INIT Failed to fetch Edupage data: %s", e) return False - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="EduPage Data", - update_method=fetch_data, - update_interval=timedelta(minutes=5), - ) + try: + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="EduPage Data", + update_method=fetch_data, + update_interval=timedelta(minutes=60), + ) + _LOGGER.info("INIT coordinator instantiated") + except Exception as e: + _LOGGER.info("INIT coordinator not instantiated") # First data fetch await asyncio.sleep(1) @@ -118,15 +152,19 @@ async def fetch_data(): hass.data[DOMAIN][entry.entry_id] = coordinator # Forward platforms - await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR, Platform.CALENDAR]) + _LOGGER.info(f"INIT Forwarding entry for platforms: calendar") + #await hass.config_entries.async_forward_entry_setups(entry, [Platform.CALENDAR]) + await hass.config_entries.async_forward_entry_setups(entry, ["calendar"]) + _LOGGER.info(f"INIT forwarded") + _LOGGER.debug(f"INIT Coordinator data: {coordinator.data}") 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) + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, ["calendar"]) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + diff --git a/custom_components/homeassistantedupage/calendar.py b/custom_components/homeassistantedupage/calendar.py index 3b1f87b..99fbf51 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -1,65 +1,132 @@ -from homeassistant.components.calendar import CalendarEventDevice -from homeassistant.helpers.entity import generate_entity_id -from homeassistant.util import dt as dt_util -from datetime import datetime, timedelta +import logging +from datetime import datetime, timedelta, timezone +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.helpers.update_coordinator import UpdateFailed +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 +from zoneinfo import ZoneInfo -# Importiere die EduPage-API-Klassen, falls nötig -# from edupage_api.models import Timetable, Lesson +_LOGGER = logging.getLogger("custom_components.homeassistant_edupage") +_LOGGER.info("CALENDAR - Edupage calendar.py is being loaded") -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the EduPage Calendar platform.""" - if discovery_info is None: - return +async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None: + """Set up Edupage calendar entities.""" + _LOGGER.info("CALENDAR called async_setup_entry") - # Füge eine Instanz des Kalenders hinzu - async_add_entities([EduPageCalendar(hass, discovery_info["name"], discovery_info["timetable_data"])]) + coordinator = hass.data[DOMAIN][entry.entry_id] + edupage_calendar = EdupageCalendar(coordinator, entry.data) -class EduPageCalendar(CalendarEventDevice): - """Representation of the EduPage Calendar.""" + async_add_entities([edupage_calendar]) - def __init__(self, hass, name, timetable_data): - """Initialize the calendar.""" - self._hass = hass - self._name = name - self._timetable_data = timetable_data + _LOGGER.info("CALENDAR async_setup_entry finished.") + + +async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + _LOGGER.info("CALENDAR added to hass") + + if self.coordinator: + self.async_on_remove( + self.coordinator.async_add_listener( + self._handle_coordinator_update, None + ) + ) + +class EdupageCalendar(CoordinatorEntity, CalendarEntity): + """Representation of an Edupage calendar entity.""" + + def __init__(self, coordinator, data): + super().__init__(coordinator) + self.coordinator = coordinator + self._data = data self._events = [] + self._attr_name = F"EdupageCal" + _LOGGER.info(f"CALENDAR Initialized EdupageCalendar with data: {data}") @property def name(self): - """Return the name of the calendar.""" - return self._name + return "Edupage Calendar" - async def async_get_events(self, hass, start_date, end_date): - """Return all events in the specified date range.""" - events = [] - for lesson in self._timetable_data.lessons: - start_time = datetime.combine(start_date, lesson.start_time) - end_time = datetime.combine(start_date, lesson.end_time) - - # Füge das Event hinzu, wenn es im Zeitfenster liegt - if start_time >= start_date and end_time <= end_date: - event = { - "title": lesson.subject.name, - "start": start_time.isoformat(), - "end": end_time.isoformat(), - "description": f"Lehrer: {', '.join([t.name for t in lesson.teachers])}", - } - events.append(event) - - self._events = events - return events + @property + def unique_id(self): + """Return a unique ID for this calendar.""" + return f"edupage_calendar_{self._data.get('subdomain', 'default')}" @property def extra_state_attributes(self): - """Extra attributes of the calendar.""" - return {"number_of_events": len(self._events)} + """Return the extra state attributes.""" + return { + "unique_id": self.unique_id, + "other_info": "debug info" + } + + @property + def available(self) -> bool: + """Return True if the calendar is available.""" + _LOGGER.debug("CALENDAR Checking availability of Edupage Calendar") + return True @property def event(self): - """Return the next upcoming event.""" - now = dt_util.now() - for event in self._events: - if datetime.fromisoformat(event["start"]) > now: - return event - return None + """Return the next upcoming event or None if no event exists.""" + local_tz = ZoneInfo(self.hass.config.time_zone) + now = datetime.now(local_tz) + return CalendarEvent( + start=now + timedelta(hours=1), + end=now + timedelta(hours=2), + summary="Next Dummy Event", + description="A placeholder event for debugging." + ) + + async def async_get_events(self, hass, start_date: datetime, end_date: datetime): + + """Return events in a specific date range.""" + local_tz = ZoneInfo(self.hass.config.time_zone) + events = [] + + _LOGGER.info(f"CALENDAR Fetching events from {start_date} to {end_date}") + + # Prüfen, ob 'timetable' im Coordinator existiert + timetable = self.coordinator.data.get("timetable") + if not timetable: + _LOGGER.warning("CALENDAR Timetable data is missing.") + return events + + # Iteriere über alle Tage im Zeitraum + current_date = start_date.date() + while current_date <= end_date.date(): + day_timetable = timetable.get(current_date) + if day_timetable and day_timetable.lessons: + # Iteriere über alle Lektionen des Tages + for lesson in day_timetable.lessons: + start = datetime.combine(current_date, lesson.start_time).replace(tzinfo=local_tz) + end = datetime.combine(current_date, lesson.end_time).replace(tzinfo=local_tz) + + # Generiere CalendarEvent + events.append( + CalendarEvent( + start=start, + end=end, + summary=lesson.subject.name if lesson.subject else "Unbekanntes Fach", + description=( + f"Lehrer: {', '.join([t.name for t in lesson.teachers])} | " + f"Raum: {', '.join([c.name for c in (lesson.classrooms or [])])}" + if lesson.teachers and lesson.classrooms + else "Keine Details verfügbar" + ), + ) + ) + else: + _LOGGER.debug(f"CALENDAR No lessons found for {current_date}") + + # Gehe zum nächsten Tag + current_date += timedelta(days=1) + + _LOGGER.info(f"CALENDAR Fetched {len(events)} events from {start_date} to {end_date}") + return events diff --git a/custom_components/homeassistantedupage/homeassistant-edupage b/custom_components/homeassistantedupage/homeassistant-edupage deleted file mode 120000 index 187fd7c..0000000 --- a/custom_components/homeassistantedupage/homeassistant-edupage +++ /dev/null @@ -1 +0,0 @@ -./homeassistant-edupage \ No newline at end of file diff --git a/custom_components/homeassistantedupage/homeassistant_edupage.py b/custom_components/homeassistantedupage/homeassistant_edupage.py index 6b0a0ac..3e2d74f 100644 --- a/custom_components/homeassistantedupage/homeassistant_edupage.py +++ b/custom_components/homeassistantedupage/homeassistant_edupage.py @@ -4,6 +4,7 @@ from edupage_api.people import EduTeacher from edupage_api.people import Gender from edupage_api.classrooms import Classroom +from zoneinfo import ZoneInfo from datetime import datetime from datetime import date @@ -77,56 +78,15 @@ async def get_teachers(self): except Exception as e: raise UpdateFailed(F"EDUPAGE error updating get_teachers data from API: {e}") - async def get_timetable(self): - try: - _LOGGER.debug("Begin creating first teacher instance") - teacher1 = EduTeacher( - person_id=-17, - name="Anka Kehr", - gender=Gender.FEMALE, - in_school_since=None, - classroom_name="Haus 1 R 08", - teacher_to=None - ) - _LOGGER.debug("First teacher instance created: %s", teacher1) - - teacher2 = EduTeacher( - person_id=-25, - name="Christiane Koch", - gender=Gender.FEMALE, - in_school_since=None, - classroom_name="Haus 1 R 08", - teacher_to=None - ) - _LOGGER.debug("Teacher2 created successfully: %s", teacher2) - - classroom = Classroom( - classroom_id=-12, - name="Haus 1 R 08", - short="H1 R08" - ) - _LOGGER.debug("Classroom created successfully: %s", classroom) - - class_instance = Class( - class_id=-28, - name="4b", - short="4b", - homeroom_teachers=[teacher1, teacher2], - homeroom=classroom, - grade=None - ) - _LOGGER.debug("Class instance created successfully: %s", class_instance) - - except Exception as e: - _LOGGER.error("Error during instantiation: %s", e) + async def get_timetable(self, class_instance, date): try: executor = ThreadPoolExecutor(max_workers=5) - timetable_data = await self.hass.async_add_executor_job(self.api.get_timetable, class_instance, date.today()) + timetable_data = await self.hass.async_add_executor_job(self.api.get_timetable, class_instance, date) if timetable_data is None: _LOGGER.info("EDUPAGE timetable is None") else: - _LOGGER.info("EDUPAGE timetable_data: $s", timetable_data) + _LOGGER.info("EDUPAGE timetable_data found") return timetable_data except Exception as e: raise UpdateFailed(F"EDUPAGE error updating get_timetable() data from API: {e}") From e7b1ca07b551ee1b4d483ffde6be0262d53365b2 Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Sun, 24 Nov 2024 15:30:40 +0100 Subject: [PATCH 4/7] working: timetable, multiple student --- .../homeassistantedupage/__init__.py | 98 ++++++------------- .../homeassistantedupage/calendar.py | 60 ++++++------ .../homeassistantedupage/config_flow.py | 77 +++++++++++++-- .../homeassistant_edupage.py | 24 +++-- .../homeassistantedupage/sensor.py | 27 +++-- 5 files changed, 158 insertions(+), 128 deletions(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index 9f34042..186843d 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -27,50 +27,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username = entry.data["username"] password = entry.data["password"] subdomain = entry.data["subdomain"] + student_id = entry.data["student_id"] edupage = Edupage(hass) - try: - # _LOGGER.debug("Begin creating first teacher instance") - teacher1 = EduTeacher( - person_id=-17, - name="Anka Kehr", - gender=Gender.FEMALE, - in_school_since=None, - classroom_name="Haus 1 R 08", - teacher_to=None - ) - # _LOGGER.debug("First teacher instance created: %s", teacher1) - - teacher2 = EduTeacher( - person_id=-25, - name="Christiane Koch", - gender=Gender.FEMALE, - in_school_since=None, - classroom_name="Haus 1 R 08", - teacher_to=None - ) - # _LOGGER.debug("Teacher2 created successfully: %s", teacher2) - - classroom = Classroom( - classroom_id=-12, - name="Haus 1 R 08", - short="H1 R08" - ) - # _LOGGER.debug("Classroom created successfully: %s", classroom) - - class_instance = Class( - class_id=-28, - name="4b", - short="4b", - homeroom_teachers=[teacher1, teacher2], - homeroom=classroom, - grade=None - ) - # _LOGGER.debug("Class instance created successfully: %s", class_instance) - - except Exception as e: - _LOGGER.error("INIT Error during instantiation: %s", e) - try: login_success = await hass.async_add_executor_job( edupage.login, username, password, subdomain @@ -92,44 +51,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: fetch_lock = asyncio.Lock() async def fetch_data(): - """Function to fetch grade and timetable data.""" - _LOGGER.info("INIT called fetch_data") + """Function to fetch timetable data for the selected student.""" + _LOGGER.info("INIT called fetch_data") async with fetch_lock: try: - # Daten abrufen - classes_data = await edupage.get_classes() - grades_data = await edupage.get_grades() - userid = await edupage.get_user_id() - subjects_data = await edupage.get_subjects() - students_data = await edupage.get_students() - teachers_data = await edupage.get_teachers() - classrooms_data = await edupage.get_classrooms() - - # Stundenplan für die nächsten 14 Tage abrufen - today = datetime.now().date() + await edupage.login(username, password, subdomain) + + students = await edupage.get_students() + student = next((s for s in students if s.person_id == student_id), None) + _LOGGER.info("INIT Student: %s", student) + + if not student: + _LOGGER.error("No matching student found with ID: %s", student_id) + return {"timetable": {}} + + _LOGGER.debug("Found EduStudent: %s", vars(student)) + timetable_data = {} + today = datetime.now().date() for offset in range(14): current_date = today + timedelta(days=offset) - timetable_data[current_date] = await edupage.get_timetable(class_instance, current_date) + timetable = await edupage.get_timetable(student, current_date) + if timetable: + _LOGGER.debug(f"Timetable for {current_date}: {timetable}") + timetable_data[current_date] = timetable + else: + _LOGGER.warning(f"No timetable found for {current_date}") - _LOGGER.info("INIT got timetables before return") - - # Daten zusammenstellen return { - "classes": classes_data, - "grades": grades_data, - "user_id": userid, - "subjects": subjects_data, - "students": students_data, - "teachers": teachers_data, - "classrooms": classrooms_data, + "student": {"id": student.person_id, "name": student.name}, "timetable": timetable_data, } except Exception as e: - _LOGGER.error("INIT Failed to fetch Edupage data: %s", e) - return False + _LOGGER.error("Failed to fetch timetable: %s", e) + return {"timetable": {}} + try: coordinator = DataUpdateCoordinator( @@ -162,8 +120,8 @@ 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, ["calendar"]) + _LOGGER.info("INIT called async_unload_entry") + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "calendar") if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/custom_components/homeassistantedupage/calendar.py b/custom_components/homeassistantedupage/calendar.py index 99fbf51..8c62d9d 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -49,15 +49,18 @@ def __init__(self, coordinator, data): self._attr_name = F"EdupageCal" _LOGGER.info(f"CALENDAR Initialized EdupageCalendar with data: {data}") - @property - def name(self): - return "Edupage Calendar" - @property def unique_id(self): """Return a unique ID for this calendar.""" - return f"edupage_calendar_{self._data.get('subdomain', 'default')}" + student_id = self.coordinator.data.get("student", {}).get("id", "unknown") + return f"edupage_calendar_{student_id}" + @property + def name(self): + """Return the name of the calendar.""" + student_name = self.coordinator.data.get("student", {}).get("name", "Unknown Student") + return f"Edupage - {student_name}" + @property def extra_state_attributes(self): """Return the extra state attributes.""" @@ -85,47 +88,44 @@ def event(self): ) async def async_get_events(self, hass, start_date: datetime, end_date: datetime): - """Return events in a specific date range.""" local_tz = ZoneInfo(self.hass.config.time_zone) events = [] - _LOGGER.info(f"CALENDAR Fetching events from {start_date} to {end_date}") - - # Prüfen, ob 'timetable' im Coordinator existiert timetable = self.coordinator.data.get("timetable") if not timetable: _LOGGER.warning("CALENDAR Timetable data is missing.") return events - # Iteriere über alle Tage im Zeitraum + # Iteriere über die Tage und Lektionen im Stundenplan current_date = start_date.date() while current_date <= end_date.date(): day_timetable = timetable.get(current_date) - if day_timetable and day_timetable.lessons: - # Iteriere über alle Lektionen des Tages - for lesson in day_timetable.lessons: - start = datetime.combine(current_date, lesson.start_time).replace(tzinfo=local_tz) - end = datetime.combine(current_date, lesson.end_time).replace(tzinfo=local_tz) - - # Generiere CalendarEvent + if day_timetable: + for lesson in day_timetable: + # Debug die Attribute des Lesson-Objekts + _LOGGER.debug(f"Lesson attributes: {vars(lesson)}") + + # Rauminformationen aus der Klasse extrahieren + room = "Unknown" + if lesson.classes and lesson.classes[0].homeroom: + room = lesson.classes[0].homeroom.name + + # Lehrerinformationen extrahieren + teacher_names = [teacher.name for teacher in lesson.teachers] + teachers = ", ".join(teacher_names) if teacher_names else "Unknown Teacher" + + # Kombiniere Datum und Zeit zu einem vollständigen datetime-Objekt + start_time = datetime.combine(current_date, lesson.start_time).astimezone(local_tz) + end_time = datetime.combine(current_date, lesson.end_time).astimezone(local_tz) events.append( CalendarEvent( - start=start, - end=end, - summary=lesson.subject.name if lesson.subject else "Unbekanntes Fach", - description=( - f"Lehrer: {', '.join([t.name for t in lesson.teachers])} | " - f"Raum: {', '.join([c.name for c in (lesson.classrooms or [])])}" - if lesson.teachers and lesson.classrooms - else "Keine Details verfügbar" - ), + start=start_time, + end=end_time, + summary=lesson.subject.name if lesson.subject else "Unknown Subject", + description=f"Room: {room}\nTeacher(s): {teachers}" ) ) - else: - _LOGGER.debug(f"CALENDAR No lessons found for {current_date}") - - # Gehe zum nächsten Tag current_date += timedelta(days=1) _LOGGER.info(f"CALENDAR Fetched {len(events)} events from {start_date} to {end_date}") diff --git a/custom_components/homeassistantedupage/config_flow.py b/custom_components/homeassistantedupage/config_flow.py index 787d3cc..37a4adc 100644 --- a/custom_components/homeassistantedupage/config_flow.py +++ b/custom_components/homeassistantedupage/config_flow.py @@ -2,16 +2,15 @@ import voluptuous as vol from edupage_api import Edupage from homeassistant import config_entries -from homeassistant.core import HomeAssistant +from homeassistant.core import callback from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -CONF_SUBDOMAIN = "subdomain" +CONF_SUBDOMAIN = "subdomain" # Lokal definiert -class EdupageConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for edupage_api.""" +class EdupageConfigFlow(config_entries.ConfigFlow, domain="homeassistantedupage"): + """Handle a config flow for Edupage.""" VERSION = 1 @@ -20,15 +19,75 @@ async def async_step_user(self, user_input=None): errors = {} if user_input is not None: + _LOGGER.info("User input received: %s", user_input) + api = Edupage() - return self.async_create_entry(title="Edupage", data=user_input) + try: + # Login ausführen + _LOGGER.debug("Starting login process") + await self.hass.async_add_executor_job( + api.login, + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + user_input[CONF_SUBDOMAIN] + ) + _LOGGER.debug("Login successful") + # Schülerliste abrufen + students = await self.hass.async_add_executor_job(api.get_students) + _LOGGER.debug("Students retrieved: %s", students) + + if not students: + errors["base"] = "no_students_found" + else: + # Speichere Benutzer-Eingaben + self.user_data = user_input + self.students = {student.person_id: student.name for student in students} + + # Weiter zur Schülerauswahl + return await self.async_step_select_student() + + except Exception as e: + _LOGGER.error("Exception during API call: %s", e) + errors["base"] = "cannot_connect" + + # Formular anzeigen data_schema = vol.Schema({ - vol.Required(CONF_USERNAME, default="username"): str, - vol.Required(CONF_PASSWORD, default="password"): str, - vol.Required(CONF_SUBDOMAIN, default="subdomain only"): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_SUBDOMAIN): str, }) return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) + + + async def async_step_select_student(self, user_input=None): + """Handle the selection of a student.""" + errors = {} + + if user_input is not None: + student_id = user_input.get("student") + _LOGGER.info("Selected student ID: %s", student_id) + + # Erstelle den Config-Entry mit allen Daten + return self.async_create_entry( + title=f"Edupage ({self.students[student_id]})", + data={ + **self.user_data, # Login-Daten hinzufügen + "student_id": student_id, + "student_name": self.students[student_id], + }, + ) + + # Dropdown-Formular für Schülerauswahl + student_schema = vol.Schema({ + vol.Required("student"): vol.In(self.students), + }) + + return self.async_show_form( + step_id="select_student", + data_schema=student_schema, + errors=errors + ) diff --git a/custom_components/homeassistantedupage/homeassistant_edupage.py b/custom_components/homeassistantedupage/homeassistant_edupage.py index 3e2d74f..b082456 100644 --- a/custom_components/homeassistantedupage/homeassistant_edupage.py +++ b/custom_components/homeassistantedupage/homeassistant_edupage.py @@ -1,4 +1,6 @@ import logging +import asyncio + from edupage_api import Edupage as APIEdupage from edupage_api.classes import Class from edupage_api.people import EduTeacher @@ -18,9 +20,14 @@ def __init__(self,hass): self.hass = hass self.api = APIEdupage() - def login(self, username, password, subdomain): + async def login(self, username: str, password: str, subdomain: str): + """Perform login asynchronously.""" + try: + return await asyncio.to_thread(self.api.login, username, password, subdomain) + except Exception as e: + _LOGGER.error(f"Failed to log in: {e}") + raise - return self.api.login(username, password, subdomain) async def get_classes(self): @@ -78,18 +85,17 @@ async def get_teachers(self): except Exception as e: raise UpdateFailed(F"EDUPAGE error updating get_teachers data from API: {e}") - async def get_timetable(self, class_instance, date): - + async def get_timetable(self, EduStudent, date): try: - executor = ThreadPoolExecutor(max_workers=5) - timetable_data = await self.hass.async_add_executor_job(self.api.get_timetable, class_instance, date) + timetable_data = await self.hass.async_add_executor_job(self.api.get_timetable, EduStudent, date) if timetable_data is None: _LOGGER.info("EDUPAGE timetable is None") else: - _LOGGER.info("EDUPAGE timetable_data found") - return timetable_data + _LOGGER.debug(f"EDUPAGE timetable_data for {date}: {timetable_data}") + return timetable_data except Exception as e: - raise UpdateFailed(F"EDUPAGE error updating get_timetable() data from API: {e}") + _LOGGER.error(f"EDUPAGE error updating get_timetable() data for {date}: {e}") + raise UpdateFailed(f"EDUPAGE error updating get_timetable() data for {date}: {e}") async def async_update(self): diff --git a/custom_components/homeassistantedupage/sensor.py b/custom_components/homeassistantedupage/sensor.py index 6334a1e..2a0df6a 100644 --- a/custom_components/homeassistantedupage/sensor.py +++ b/custom_components/homeassistantedupage/sensor.py @@ -17,12 +17,13 @@ def group_grades_by_subject(grades): return grouped async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None: - """Set up EduPage sensors based on subjects and their grades.""" + """Set up EduPage sensors for each student and their grades.""" _LOGGER.info("SENSOR called async_setup_entry") coordinator = hass.data[DOMAIN][entry.entry_id] + student = coordinator.data.get("student", {}) subjects = coordinator.data.get("subjects", []) - grades = coordinator.data.get("grades", []) + grades = coordinator.data.get("grades", []) # group grades based on subject_id grades_by_subject = group_grades_by_subject(grades) @@ -33,6 +34,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e subject_grades = grades_by_subject.get(subject.subject_id, []) sensor = EduPageSubjectSensor( coordinator, + student.get("id"), + student.get("name"), subject.name, subject_grades ) @@ -40,29 +43,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e async_add_entities(sensors, True) + class EduPageSubjectSensor(CoordinatorEntity, SensorEntity): - """subject sensor entity.""" + """Subject sensor entity for a specific student.""" - def __init__(self, coordinator, subject_name, grades=None): - """initializing""" + def __init__(self, coordinator, student_id, student_name, subject_name, grades=None): + """Initialize the sensor.""" super().__init__(coordinator) + self._student_id = student_id + self._student_name = student_name self._subject_name = subject_name self._grades = grades or [] - self._attr_name = f"EduPage subject - {subject_name}" - self._attr_unique_id = f"edupage_grades_{subject_name.lower().replace(' ', '_')}" + self._attr_name = f"{student_name} - {subject_name}" + self._attr_unique_id = f"edupage_grades_{student_id}_{subject_name.lower().replace(' ', '_')}" @property def state(self): - """return grade count""" + """Return the grade count.""" return len(self._grades) @property def extra_state_attributes(self): - """return additional attributes""" + """Return additional attributes.""" if not self._grades: return {"info": "no grades yet"} - attributes = {} + attributes = {"student": self._student_name} 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 @@ -70,3 +76,4 @@ def extra_state_attributes(self): attributes[f"grade_{i+1}_teacher"] = grade.teacher.name return attributes + From fca10e741b0e78c9d60f3402a47a4dc7642ca7bd Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Sun, 24 Nov 2024 20:46:02 +0100 Subject: [PATCH 5/7] its wacky but sensor and calendar working --- .../homeassistantedupage/__init__.py | 49 ++++++++++++++----- .../homeassistantedupage/calendar.py | 17 ++++--- .../homeassistant_edupage.py | 25 +++++++--- 3 files changed, 64 insertions(+), 27 deletions(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index 186843d..3e242ac 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -23,12 +23,15 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """initializin EduPage-integration and validate API-login""" _LOGGER.info("INIT called async_setup_entry") + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} # Initialisiere den Speicherplatz für die Integration username = entry.data["username"] password = entry.data["password"] subdomain = entry.data["subdomain"] student_id = entry.data["student_id"] edupage = Edupage(hass) + coordinator = None try: login_success = await hass.async_add_executor_job( @@ -63,10 +66,13 @@ async def fetch_data(): _LOGGER.info("INIT Student: %s", student) if not student: - _LOGGER.error("No matching student found with ID: %s", student_id) + _LOGGER.error("INIT No matching student found with ID: %s", student_id) return {"timetable": {}} - _LOGGER.debug("Found EduStudent: %s", vars(student)) + #_LOGGER.debug("INIT Found EduStudent: %s", vars(student)) + + grades = await edupage.get_grades() + subjects = await edupage.get_subjects() timetable_data = {} today = datetime.now().date() @@ -74,36 +80,53 @@ async def fetch_data(): current_date = today + timedelta(days=offset) timetable = await edupage.get_timetable(student, current_date) if timetable: - _LOGGER.debug(f"Timetable for {current_date}: {timetable}") + #_LOGGER.debug(f"Timetable for {current_date}: {timetable}") timetable_data[current_date] = timetable else: - _LOGGER.warning(f"No timetable found for {current_date}") + _LOGGER.warning(f"INIT No timetable found for {current_date}") - return { + return_data = { "student": {"id": student.person_id, "name": student.name}, + "grades": grades, + "subjects": subjects, "timetable": timetable_data, } + # _LOGGER.debug(f"INIIIIIIIIIIIIIIIIT Coordinator fetch_data returning: {return_data}") + return return_data except Exception as e: - _LOGGER.error("Failed to fetch timetable: %s", e) + _LOGGER.error("INIT Failed to fetch timetable: %s", e) return {"timetable": {}} - try: + # Erstelle den Coordinator coordinator = DataUpdateCoordinator( hass, _LOGGER, - name="EduPage Data", + name="Edupage", update_method=fetch_data, - update_interval=timedelta(minutes=60), + update_interval=timedelta(minutes=5), ) - _LOGGER.info("INIT coordinator instantiated") + hass.data[DOMAIN][entry.entry_id] = coordinator + + # Starte die erste Datenabfrage + await coordinator.async_config_entry_first_refresh() + _LOGGER.info("INIT Coordinator successfully initialized") + except Exception as e: - _LOGGER.info("INIT coordinator not instantiated") + _LOGGER.error(f"INIT Error during async_setup_entry: {e}") + + # Entferne unvollständige Einträge + if entry.entry_id in hass.data[DOMAIN]: + del hass.data[DOMAIN][entry.entry_id] + + return False + - # First data fetch + ## First data fetch await asyncio.sleep(1) await coordinator.async_config_entry_first_refresh() + #_LOGGER.info(f"INIT Coordinator data after first fetch!") # Save coordinator hass.data.setdefault(DOMAIN, {}) @@ -112,7 +135,7 @@ async def fetch_data(): # Forward platforms _LOGGER.info(f"INIT Forwarding entry for platforms: calendar") #await hass.config_entries.async_forward_entry_setups(entry, [Platform.CALENDAR]) - await hass.config_entries.async_forward_entry_setups(entry, ["calendar"]) + await hass.config_entries.async_forward_entry_setups(entry, ["calendar", "sensor"]) _LOGGER.info(f"INIT forwarded") _LOGGER.debug(f"INIT Coordinator data: {coordinator.data}") diff --git a/custom_components/homeassistantedupage/calendar.py b/custom_components/homeassistantedupage/calendar.py index 8c62d9d..90bdb1c 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -11,7 +11,7 @@ from zoneinfo import ZoneInfo _LOGGER = logging.getLogger("custom_components.homeassistant_edupage") -_LOGGER.info("CALENDAR - Edupage calendar.py is being loaded") +_LOGGER.info("CALENDAR Edupage calendar.py is being loaded") async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None: """Set up Edupage calendar entities.""" @@ -25,7 +25,6 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback _LOGGER.info("CALENDAR async_setup_entry finished.") - async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() @@ -43,12 +42,12 @@ class EdupageCalendar(CoordinatorEntity, CalendarEntity): def __init__(self, coordinator, data): super().__init__(coordinator) - self.coordinator = coordinator - self._data = data + self._data = data # Optional, falls du die Daten direkt verwenden möchtest. self._events = [] - self._attr_name = F"EdupageCal" + self._attr_name = "Edupage Calendar" # Klare und lesbare Benennung. _LOGGER.info(f"CALENDAR Initialized EdupageCalendar with data: {data}") + @property def unique_id(self): """Return a unique ID for this calendar.""" @@ -92,7 +91,11 @@ async def async_get_events(self, hass, start_date: datetime, end_date: datetime) local_tz = ZoneInfo(self.hass.config.time_zone) events = [] - timetable = self.coordinator.data.get("timetable") + _LOGGER.debug(f"CALENDAR Fetching events between {start_date} and {end_date}") + timetable = self.coordinator.data.get("timetable", {}) + _LOGGER.debug(f"CALENDAR Coordinator data: {self.coordinator.data}") + _LOGGER.debug(f"CALENDAR Fetched timetable data: {timetable}") + if not timetable: _LOGGER.warning("CALENDAR Timetable data is missing.") return events @@ -104,7 +107,7 @@ async def async_get_events(self, hass, start_date: datetime, end_date: datetime) if day_timetable: for lesson in day_timetable: # Debug die Attribute des Lesson-Objekts - _LOGGER.debug(f"Lesson attributes: {vars(lesson)}") + _LOGGER.debug(f"CALENDAR Lesson attributes: {vars(lesson)}") # Rauminformationen aus der Klasse extrahieren room = "Unknown" diff --git a/custom_components/homeassistantedupage/homeassistant_edupage.py b/custom_components/homeassistantedupage/homeassistant_edupage.py index b082456..5b0868c 100644 --- a/custom_components/homeassistantedupage/homeassistant_edupage.py +++ b/custom_components/homeassistantedupage/homeassistant_edupage.py @@ -7,6 +7,8 @@ from edupage_api.people import Gender from edupage_api.classrooms import Classroom from zoneinfo import ZoneInfo +from edupage_api.exceptions import BadCredentialsException, CaptchaException + from datetime import datetime from datetime import date @@ -23,11 +25,20 @@ def __init__(self,hass): async def login(self, username: str, password: str, subdomain: str): """Perform login asynchronously.""" try: - return await asyncio.to_thread(self.api.login, username, password, subdomain) - except Exception as e: - _LOGGER.error(f"Failed to log in: {e}") - raise + result = await asyncio.to_thread(self.api.login, username, password, subdomain) + _LOGGER.debug(f"EDUPAGE Login successful, result: {result}") + return result + except BadCredentialsException as e: + _LOGGER.error("INIT login failed: bad credentials. %s", e) + return False + except CaptchaException as e: + _LOGGER.error("INIT login failed: CAPTCHA needed. %s", e) + return False + + except Exception as e: + _LOGGER.error("INIT unexpected login error: %s", e) + return False async def get_classes(self): @@ -91,10 +102,10 @@ async def get_timetable(self, EduStudent, date): if timetable_data is None: _LOGGER.info("EDUPAGE timetable is None") else: - _LOGGER.debug(f"EDUPAGE timetable_data for {date}: {timetable_data}") - return timetable_data + #_LOGGER.debug(f"EDUPAGE timetable_data for {date}: {timetable_data}") + return timetable_data except Exception as e: - _LOGGER.error(f"EDUPAGE error updating get_timetable() data for {date}: {e}") + #_LOGGER.error(f"EDUPAGE error updating get_timetable() data for {date}: {e}") raise UpdateFailed(f"EDUPAGE error updating get_timetable() data for {date}: {e}") async def async_update(self): From 0fc9648a26a76e164205cfc9ea7e47af1cf0033b Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Sun, 24 Nov 2024 20:55:13 +0100 Subject: [PATCH 6/7] fixed loglevels and housekeeping --- .../homeassistantedupage/__init__.py | 37 +++++++------------ .../homeassistantedupage/calendar.py | 23 +++++------- .../homeassistant_edupage.py | 12 +++--- .../homeassistantedupage/sensor.py | 4 +- 4 files changed, 30 insertions(+), 46 deletions(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index 3e242ac..4f44a7a 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -17,14 +17,14 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: """only ConfigEntry supported, no configuration.yaml yet""" - _LOGGER.info("INIT called async_setup") + _LOGGER.debug("INIT called async_setup") return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """initializin EduPage-integration and validate API-login""" - _LOGGER.info("INIT called async_setup_entry") + _LOGGER.debug("INIT called async_setup_entry") if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} # Initialisiere den Speicherplatz für die Integration + hass.data[DOMAIN] = {} username = entry.data["username"] password = entry.data["password"] @@ -37,7 +37,7 @@ 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") + _LOGGER.debug("INIT login_success") except BadCredentialsException as e: _LOGGER.error("INIT login failed: bad credentials. %s", e) @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def fetch_data(): """Function to fetch timetable data for the selected student.""" - _LOGGER.info("INIT called fetch_data") + _LOGGER.debug("INIT called fetch_data") async with fetch_lock: try: @@ -63,13 +63,13 @@ async def fetch_data(): students = await edupage.get_students() student = next((s for s in students if s.person_id == student_id), None) - _LOGGER.info("INIT Student: %s", student) + _LOGGER.debug("INIT Student: %s", student) if not student: _LOGGER.error("INIT No matching student found with ID: %s", student_id) return {"timetable": {}} - #_LOGGER.debug("INIT Found EduStudent: %s", vars(student)) + _LOGGER.debug("INIT Found EduStudent: %s", vars(student)) grades = await edupage.get_grades() subjects = await edupage.get_subjects() @@ -80,7 +80,7 @@ async def fetch_data(): current_date = today + timedelta(days=offset) timetable = await edupage.get_timetable(student, current_date) if timetable: - #_LOGGER.debug(f"Timetable for {current_date}: {timetable}") + _LOGGER.debug(f"Timetable for {current_date}: {timetable}") timetable_data[current_date] = timetable else: _LOGGER.warning(f"INIT No timetable found for {current_date}") @@ -91,7 +91,7 @@ async def fetch_data(): "subjects": subjects, "timetable": timetable_data, } - # _LOGGER.debug(f"INIIIIIIIIIIIIIIIIT Coordinator fetch_data returning: {return_data}") + _LOGGER.debug(f"INIT Coordinator fetch_data returning: {return_data}") return return_data except Exception as e: @@ -99,7 +99,6 @@ async def fetch_data(): return {"timetable": {}} try: - # Erstelle den Coordinator coordinator = DataUpdateCoordinator( hass, _LOGGER, @@ -109,42 +108,34 @@ async def fetch_data(): ) hass.data[DOMAIN][entry.entry_id] = coordinator - # Starte die erste Datenabfrage await coordinator.async_config_entry_first_refresh() - _LOGGER.info("INIT Coordinator successfully initialized") + _LOGGER.debug("INIT Coordinator successfully initialized") except Exception as e: _LOGGER.error(f"INIT Error during async_setup_entry: {e}") - # Entferne unvollständige Einträge if entry.entry_id in hass.data[DOMAIN]: del hass.data[DOMAIN][entry.entry_id] return False - - ## First data fetch await asyncio.sleep(1) await coordinator.async_config_entry_first_refresh() - #_LOGGER.info(f"INIT Coordinator data after first fetch!") + _LOGGER.debug(f"INIT Coordinator first fetch!") - # Save coordinator hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - # Forward platforms - _LOGGER.info(f"INIT Forwarding entry for platforms: calendar") - #await hass.config_entries.async_forward_entry_setups(entry, [Platform.CALENDAR]) await hass.config_entries.async_forward_entry_setups(entry, ["calendar", "sensor"]) - _LOGGER.info(f"INIT forwarded") + _LOGGER.debug(f"INIT forwarded") _LOGGER.debug(f"INIT Coordinator data: {coordinator.data}") 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, "calendar") + _LOGGER.debug("INIT called async_unload_entry") + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, ["calendar", "sensor"]) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/custom_components/homeassistantedupage/calendar.py b/custom_components/homeassistantedupage/calendar.py index 90bdb1c..1699e8f 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -11,11 +11,11 @@ from zoneinfo import ZoneInfo _LOGGER = logging.getLogger("custom_components.homeassistant_edupage") -_LOGGER.info("CALENDAR Edupage calendar.py is being loaded") +_LOGGER.debug("CALENDAR Edupage calendar.py is being loaded") async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None: """Set up Edupage calendar entities.""" - _LOGGER.info("CALENDAR called async_setup_entry") + _LOGGER.debug("CALENDAR called async_setup_entry") coordinator = hass.data[DOMAIN][entry.entry_id] @@ -23,12 +23,12 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback async_add_entities([edupage_calendar]) - _LOGGER.info("CALENDAR async_setup_entry finished.") + _LOGGER.debug("CALENDAR async_setup_entry finished.") async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() - _LOGGER.info("CALENDAR added to hass") + _LOGGER.debug("CALENDAR added to hass") if self.coordinator: self.async_on_remove( @@ -42,11 +42,10 @@ class EdupageCalendar(CoordinatorEntity, CalendarEntity): def __init__(self, coordinator, data): super().__init__(coordinator) - self._data = data # Optional, falls du die Daten direkt verwenden möchtest. + self._data = data self._events = [] - self._attr_name = "Edupage Calendar" # Klare und lesbare Benennung. - _LOGGER.info(f"CALENDAR Initialized EdupageCalendar with data: {data}") - + self._attr_name = "Edupage Calendar" + _LOGGER.debug(f"CALENDAR Initialized EdupageCalendar with data: {data}") @property def unique_id(self): @@ -100,25 +99,21 @@ async def async_get_events(self, hass, start_date: datetime, end_date: datetime) _LOGGER.warning("CALENDAR Timetable data is missing.") return events - # Iteriere über die Tage und Lektionen im Stundenplan current_date = start_date.date() while current_date <= end_date.date(): day_timetable = timetable.get(current_date) if day_timetable: for lesson in day_timetable: - # Debug die Attribute des Lesson-Objekts + _LOGGER.debug(f"CALENDAR Lesson attributes: {vars(lesson)}") - # Rauminformationen aus der Klasse extrahieren room = "Unknown" if lesson.classes and lesson.classes[0].homeroom: room = lesson.classes[0].homeroom.name - # Lehrerinformationen extrahieren teacher_names = [teacher.name for teacher in lesson.teachers] teachers = ", ".join(teacher_names) if teacher_names else "Unknown Teacher" - # Kombiniere Datum und Zeit zu einem vollständigen datetime-Objekt start_time = datetime.combine(current_date, lesson.start_time).astimezone(local_tz) end_time = datetime.combine(current_date, lesson.end_time).astimezone(local_tz) events.append( @@ -131,5 +126,5 @@ async def async_get_events(self, hass, start_date: datetime, end_date: datetime) ) current_date += timedelta(days=1) - _LOGGER.info(f"CALENDAR Fetched {len(events)} events from {start_date} to {end_date}") + _LOGGER.debug(f"CALENDAR Fetched {len(events)} events from {start_date} to {end_date}") return events diff --git a/custom_components/homeassistantedupage/homeassistant_edupage.py b/custom_components/homeassistantedupage/homeassistant_edupage.py index 5b0868c..74057ce 100644 --- a/custom_components/homeassistantedupage/homeassistant_edupage.py +++ b/custom_components/homeassistantedupage/homeassistant_edupage.py @@ -29,15 +29,15 @@ async def login(self, username: str, password: str, subdomain: str): _LOGGER.debug(f"EDUPAGE Login successful, result: {result}") return result except BadCredentialsException as e: - _LOGGER.error("INIT login failed: bad credentials. %s", e) + _LOGGER.error("EDUPAGE login failed: bad credentials. %s", e) return False except CaptchaException as e: - _LOGGER.error("INIT login failed: CAPTCHA needed. %s", e) + _LOGGER.error("EDUPAGE login failed: CAPTCHA needed. %s", e) return False except Exception as e: - _LOGGER.error("INIT unexpected login error: %s", e) + _LOGGER.error("EDUPAGE unexpected login error: %s", e) return False async def get_classes(self): @@ -100,12 +100,12 @@ async def get_timetable(self, EduStudent, date): try: timetable_data = await self.hass.async_add_executor_job(self.api.get_timetable, EduStudent, date) if timetable_data is None: - _LOGGER.info("EDUPAGE timetable is None") + _LOGGER.debug("EDUPAGE timetable is None") else: - #_LOGGER.debug(f"EDUPAGE timetable_data for {date}: {timetable_data}") + _LOGGER.debug(f"EDUPAGE timetable_data for {date}: {timetable_data}") return timetable_data except Exception as e: - #_LOGGER.error(f"EDUPAGE error updating get_timetable() data for {date}: {e}") + _LOGGER.error(f"EDUPAGE error updating get_timetable() data for {date}: {e}") raise UpdateFailed(f"EDUPAGE error updating get_timetable() data for {date}: {e}") async def async_update(self): diff --git a/custom_components/homeassistantedupage/sensor.py b/custom_components/homeassistantedupage/sensor.py index 2a0df6a..47f7062 100644 --- a/custom_components/homeassistantedupage/sensor.py +++ b/custom_components/homeassistantedupage/sensor.py @@ -18,19 +18,17 @@ def group_grades_by_subject(grades): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None: """Set up EduPage sensors for each student and their grades.""" - _LOGGER.info("SENSOR called async_setup_entry") + _LOGGER.debug("SENSOR called async_setup_entry") coordinator = hass.data[DOMAIN][entry.entry_id] student = coordinator.data.get("student", {}) subjects = coordinator.data.get("subjects", []) grades = coordinator.data.get("grades", []) - # 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, From 96a38a85daaf4a1010fc06eb3bce6e4f1eeb0b41 Mon Sep 17 00:00:00 2001 From: Rene Lange Date: Sun, 24 Nov 2024 20:57:45 +0100 Subject: [PATCH 7/7] update interval reduced to30min, later will be configurable --- custom_components/homeassistantedupage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index 4f44a7a..7cd9b51 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -104,7 +104,7 @@ async def fetch_data(): _LOGGER, name="Edupage", update_method=fetch_data, - update_interval=timedelta(minutes=5), + update_interval=timedelta(minutes=30), ) hass.data[DOMAIN][entry.entry_id] = coordinator