From b9dad9bb130411be024e3cba8f3133973f7db3d1 Mon Sep 17 00:00:00 2001 From: Filip Marek Date: Tue, 17 Dec 2024 09:49:59 +0100 Subject: [PATCH 1/4] calendar for canteen lunches --- .../homeassistantedupage/__init__.py | 17 ++++ .../homeassistantedupage/calendar.py | 96 ++++++++++++++++++- .../homeassistant_edupage.py | 20 +++- 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index abf07bc..bec028d 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -6,6 +6,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .homeassistant_edupage import Edupage +from edupage_api.lunches import Lunch from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN, CONF_PHPSESSID, CONF_SUBDOMAIN, CONF_STUDENT_ID @@ -93,11 +94,27 @@ async def fetch_data(): if canceled_lessons: timetable_data_canceled[current_date] = canceled_lessons + canteen_menu_data = {} + for offset in range(14): + current_date = today + timedelta(days=offset) + lunch = await edupage.get_lunches(current_date) + meals_to_add = [] + if lunch is not None and lunch.menus is not None and len(lunch.menus) > 0: + _LOGGER.debug(f"Lunch for {current_date}: {lunch}") + meals_to_add.append(lunch) + + if meals_to_add: + _LOGGER.debug(f"Daily menu for {current_date}: {lessons_to_add}") + canteen_menu_data[current_date] = meals_to_add + else: + _LOGGER.warning(f"INIT No daily menu found for {current_date}") + return_data = { "student": {"id": student.person_id, "name": student.name}, "grades": grades, "subjects": subjects, "timetable": timetable_data, + "canteen_menu": canteen_menu_data, "cancelled_lessons": timetable_data_canceled, "notifications": notifications, } diff --git a/custom_components/homeassistantedupage/calendar.py b/custom_components/homeassistantedupage/calendar.py index 4709805..13580dc 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -8,6 +8,7 @@ from .const import DOMAIN from zoneinfo import ZoneInfo from edupage_api.timetables import Lesson +from edupage_api.lunches import Lunch _LOGGER = logging.getLogger("custom_components.homeassistant_edupage") _LOGGER.debug("CALENDAR Edupage calendar.py is being loaded") @@ -18,8 +19,10 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback coordinator = hass.data[DOMAIN][entry.entry_id] - edupage_calendar = EdupageCalendar(coordinator, entry.data) + edupage_canteen_calendar = EdupageCanteenCalendar(coordinator, entry.data) + async_add_entities([edupage_canteen_calendar]) + edupage_calendar = EdupageCalendar(coordinator, entry.data) async_add_entities([edupage_calendar]) _LOGGER.debug("CALENDAR async_setup_entry finished.") @@ -170,3 +173,94 @@ def find_lesson_now_or_next_across_days(self) -> Optional[CalendarEvent]: return self.map_lesson_to_calender_event(next_lesson, day) return None + +class EdupageCanteenCalendar(CoordinatorEntity, CalendarEntity): + """Representation of an Edupage canteen calendar entity.""" + + def __init__(self, coordinator, data): + super().__init__(coordinator) + self._data = data + self._events = [] + self._attr_name = "Edupage Canteen Calendar" + _LOGGER.debug(f"CALENDAR Initialized EdupageCanteenCalendar with data: {data}") + + @property + def unique_id(self): + """Return a unique ID for this calendar.""" + return f"edupage_canteen_calendar" + + @property + def name(self): + """Return the name of the calendar.""" + return f"Edupage - Canteen" + + @property + def extra_state_attributes(self): + """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 Canteen Calendar") + return True + + @property + def event(self): + """Return the next upcoming event or None if no event exists.""" + return self.find_meal_now_or_next_across_days() + + async def async_get_events(self, hass, start_date: datetime, end_date: datetime): + """Return events in a specific date range.""" + events = [] + + _LOGGER.debug(f"CALENDAR Fetching canteen calendar between {start_date} and {end_date}") + canteen_menu = self.coordinator.data.get("canteen_menu", {}) + _LOGGER.debug(f"CALENDAR Coordinator data: {self.coordinator.data}") + _LOGGER.debug(f"CALENDAR Fetched canteen_menu data: {canteen_menu}") + + if not canteen_menu: + _LOGGER.warning("CALENDAR Canteen menu data is missing.") + return events + + current_date = start_date.date() + while current_date <= end_date.date(): + events.extend(self.get_events(canteen_menu, current_date)) + current_date += timedelta(days=1) + + _LOGGER.debug(f"CALENDAR Fetched {len(events)} events from {start_date} to {end_date}") + return events + + def get_events(self, canteen_menu, current_date): + events = [] + daily_menu = canteen_menu.get(current_date) + if daily_menu: + for meal in daily_menu: + _LOGGER.debug(f"CALENDAR Meal attributes: {vars(meal)}") + events.append( + self.map_meal_to_calender_event(meal, current_date) + ) + return events + + + def map_meal_to_calender_event(self, meal: Lunch, day: date) -> CalendarEvent: + local_tz = ZoneInfo(self.hass.config.time_zone) + start_time = datetime.combine(day, meal.served_from.time()).astimezone(local_tz) + end_time = datetime.combine(day, meal.served_to.time()).astimezone(local_tz) + summary = "Lunch" + description = meal.title + + cal_event = CalendarEvent( + start=start_time, + end=end_time, + summary=summary, + description=description + ) + return cal_event + + def find_meal_now_or_next_across_days(self) -> Optional[CalendarEvent]: + # TODO implement + return None \ No newline at end of file diff --git a/custom_components/homeassistantedupage/homeassistant_edupage.py b/custom_components/homeassistantedupage/homeassistant_edupage.py index e90bafa..ab05b31 100644 --- a/custom_components/homeassistantedupage/homeassistant_edupage.py +++ b/custom_components/homeassistantedupage/homeassistant_edupage.py @@ -34,16 +34,16 @@ async def login(self, username: str, password: str, subdomain: str): except CaptchaException as e: _LOGGER.error("EDUPAGE login failed: CAPTCHA needed. %s", e) - return False + return False except SecondFactorFailedException as e: #TODO hier müsste man dann irgendwie abfangen, falls die session mal abgelaufen ist. und dies dann auch irgendwie via HA sauber zum Nutzer bringen!? _LOGGER.error("EDUPAGE login failed: 2FA error. %s", e) - return False + return False except Exception as e: _LOGGER.error("EDUPAGE unexpected login error: %s", e) - return False + return False async def get_classes(self): @@ -101,7 +101,7 @@ async def get_classrooms(self): 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: @@ -122,6 +122,18 @@ async def get_timetable(self, EduStudent, date): _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 get_lunches(self, date): + try: + lunches_data = await self.hass.async_add_executor_job(self.api.get_lunches, date) + if lunches_data is None: + _LOGGER.debug("EDUPAGE lunches is None") + else: + _LOGGER.debug(f"EDUPAGE lunches_data for {date}: {lunches_data}") + return lunches_data + except Exception as e: + _LOGGER.error(f"EDUPAGE error updating get_lunches() data for {date}: {e}") + raise UpdateFailed(f"EDUPAGE error updating get_lunches() data for {date}: {e}") + async def async_update(self): pass From dcea12a175c6722ce0fb187b9bf1ea66e38e92ae Mon Sep 17 00:00:00 2001 From: Filip Marek Date: Tue, 17 Dec 2024 15:59:28 +0100 Subject: [PATCH 2/4] load canteen calendar only if there was no exception --- custom_components/homeassistantedupage/__init__.py | 9 ++++++++- custom_components/homeassistantedupage/calendar.py | 10 +++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index bec028d..7214860 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -95,9 +95,15 @@ async def fetch_data(): timetable_data_canceled[current_date] = canceled_lessons canteen_menu_data = {} + canteen_calendar_enabled = True for offset in range(14): current_date = today + timedelta(days=offset) - lunch = await edupage.get_lunches(current_date) + try: + lunch = await edupage.get_lunches(current_date) + except Exception as e: + _LOGGER.error(f"Failed to fetch lunch data for {current_date}: {e}") + lunch = None + canteen_calendar_enabled = False meals_to_add = [] if lunch is not None and lunch.menus is not None and len(lunch.menus) > 0: _LOGGER.debug(f"Lunch for {current_date}: {lunch}") @@ -115,6 +121,7 @@ async def fetch_data(): "subjects": subjects, "timetable": timetable_data, "canteen_menu": canteen_menu_data, + "canteen_calendar_enabled": canteen_calendar_enabled, "cancelled_lessons": timetable_data_canceled, "notifications": notifications, } diff --git a/custom_components/homeassistantedupage/calendar.py b/custom_components/homeassistantedupage/calendar.py index 13580dc..283e4b6 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -19,12 +19,16 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback coordinator = hass.data[DOMAIN][entry.entry_id] - edupage_canteen_calendar = EdupageCanteenCalendar(coordinator, entry.data) - async_add_entities([edupage_canteen_calendar]) - edupage_calendar = EdupageCalendar(coordinator, entry.data) async_add_entities([edupage_calendar]) + if coordinator.data.get("canteen_calendar_enabled", {}): + edupage_canteen_calendar = EdupageCanteenCalendar(coordinator, entry.data) + async_add_entities([edupage_canteen_calendar]) + _LOGGER.debug("Canteen calendar added") + else: + _LOGGER.debug("Canteen calendar skipped, calendar disabled due to exceptions") + _LOGGER.debug("CALENDAR async_setup_entry finished.") async def async_added_to_hass(self) -> None: From 2ec4b0d1f8ba129a6626cd47e4d6830adc2a0c39 Mon Sep 17 00:00:00 2001 From: Filip Marek Date: Tue, 17 Dec 2024 19:11:35 +0100 Subject: [PATCH 3/4] disable canteen calendar after first error --- custom_components/homeassistantedupage/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index 7214860..a291bb9 100644 --- a/custom_components/homeassistantedupage/__init__.py +++ b/custom_components/homeassistantedupage/__init__.py @@ -104,6 +104,7 @@ async def fetch_data(): _LOGGER.error(f"Failed to fetch lunch data for {current_date}: {e}") lunch = None canteen_calendar_enabled = False + break meals_to_add = [] if lunch is not None and lunch.menus is not None and len(lunch.menus) > 0: _LOGGER.debug(f"Lunch for {current_date}: {lunch}") From 4918b9009a593605d1adc0c28647cc690bf48bf1 Mon Sep 17 00:00:00 2001 From: Filip Marek Date: Tue, 17 Dec 2024 19:37:08 +0100 Subject: [PATCH 4/4] calendar async_setup_entry cleanup --- custom_components/homeassistantedupage/calendar.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/custom_components/homeassistantedupage/calendar.py b/custom_components/homeassistantedupage/calendar.py index 283e4b6..eb7b36c 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -5,6 +5,8 @@ from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from .const import DOMAIN from zoneinfo import ZoneInfo from edupage_api.timetables import Lesson @@ -13,22 +15,26 @@ _LOGGER = logging.getLogger("custom_components.homeassistant_edupage") _LOGGER.debug("CALENDAR Edupage calendar.py is being loaded") -async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None: """Set up Edupage calendar entities.""" _LOGGER.debug("CALENDAR called async_setup_entry") coordinator = hass.data[DOMAIN][entry.entry_id] + calendars = [] + edupage_calendar = EdupageCalendar(coordinator, entry.data) - async_add_entities([edupage_calendar]) + calendars.append(edupage_calendar) if coordinator.data.get("canteen_calendar_enabled", {}): edupage_canteen_calendar = EdupageCanteenCalendar(coordinator, entry.data) - async_add_entities([edupage_canteen_calendar]) + calendars.append(edupage_canteen_calendar) _LOGGER.debug("Canteen calendar added") else: _LOGGER.debug("Canteen calendar skipped, calendar disabled due to exceptions") + async_add_entities(calendars) + _LOGGER.debug("CALENDAR async_setup_entry finished.") async def async_added_to_hass(self) -> None: