diff --git a/custom_components/homeassistantedupage/__init__.py b/custom_components/homeassistantedupage/__init__.py index abf07bc..a291bb9 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,35 @@ async def fetch_data(): if canceled_lessons: 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) + 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 + 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}") + 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, + "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 4709805..eb7b36c 100644 --- a/custom_components/homeassistantedupage/calendar.py +++ b/custom_components/homeassistantedupage/calendar.py @@ -5,22 +5,35 @@ 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 +from edupage_api.lunches import Lunch _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) + calendars.append(edupage_calendar) + + if coordinator.data.get("canteen_calendar_enabled", {}): + edupage_canteen_calendar = EdupageCanteenCalendar(coordinator, entry.data) + calendars.append(edupage_canteen_calendar) + _LOGGER.debug("Canteen calendar added") + else: + _LOGGER.debug("Canteen calendar skipped, calendar disabled due to exceptions") - async_add_entities([edupage_calendar]) + async_add_entities(calendars) _LOGGER.debug("CALENDAR async_setup_entry finished.") @@ -170,3 +183,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