diff --git a/custom_components/pronote/const.py b/custom_components/pronote/const.py index ae42e58..2776a50 100644 --- a/custom_components/pronote/const.py +++ b/custom_components/pronote/const.py @@ -20,5 +20,6 @@ DEFAULT_REFRESH_INTERVAL = 15 DEFAULT_ALARM_OFFSET = 60 DEFAULT_LUNCH_BREAK_TIME = "13:00" +TIMETABLE_PERIOD_MAX_LESSONS = 16 PLATFORMS = [Platform.SENSOR, Platform.CALENDAR] diff --git a/custom_components/pronote/coordinator.py b/custom_components/pronote/coordinator.py index f963e7c..15fd321 100644 --- a/custom_components/pronote/coordinator.py +++ b/custom_components/pronote/coordinator.py @@ -93,8 +93,27 @@ def get_evaluations(period): def get_overall_average(period): + """Return overall average as a real number. + + Some Pronote backends expose the overall average using a localized string + (e.g. "13,2"). Home Assistant numeric_state conditions require a value + that can be parsed as a float, so we normalise the decimal separator here. + """ try: - return period.overall_average + overall = period.overall_average + if overall is None: + return None + + # If Pronote returns a localized string like "13,2", normalise it. + if isinstance(overall, str): + normalized = overall.replace(",", ".") + try: + return float(normalized) + except (TypeError, ValueError): + # If parsing fails, fall back to the raw value. + return overall + + return overall except Exception as ex: _LOGGER.info( "Error getting overall average from period (%s): %s", period.name, ex diff --git a/custom_components/pronote/pronote_formatter.py b/custom_components/pronote/pronote_formatter.py index fc48322..1b1a91a 100644 --- a/custom_components/pronote/pronote_formatter.py +++ b/custom_components/pronote/pronote_formatter.py @@ -46,6 +46,23 @@ def format_lesson(lesson, lunch_break_time): "is_afternoon": lesson.start.time() >= lunch_break_time, } +def format_compact_lesson(lesson, lunch_break_time): + """Compact representation of a lesson for long-range timetable sensors. + + Only keep timing and label fields to reduce attribute size. + """ + return { + "start_at": lesson.start, + "end_at": lesson.end, + "start_time": lesson.start.strftime("%H:%M"), + "end_time": lesson.end.strftime("%H:%M"), + "lesson": format_displayed_lesson(lesson), + "classroom": lesson.classroom, + "canceled": lesson.canceled, + "status": lesson.status, + "is_morning": lesson.start.time() < lunch_break_time, + "is_afternoon": lesson.start.time() >= lunch_break_time, + } def format_attachment_list(attachments): return [ diff --git a/custom_components/pronote/sensor.py b/custom_components/pronote/sensor.py index 6ff04c4..5212900 100644 --- a/custom_components/pronote/sensor.py +++ b/custom_components/pronote/sensor.py @@ -21,6 +21,7 @@ GRADES_TO_DISPLAY, EVALUATIONS_TO_DISPLAY, DEFAULT_LUNCH_BREAK_TIME, + TIMETABLE_PERIOD_MAX_LESSONS, ) @@ -290,7 +291,13 @@ def __init__( @property def extra_state_attributes(self): - """Return the state attributes.""" + """Return the state attributes. + + For daily timetable sensors (today, tomorrow, next day), we expose all lessons. + For the period timetable sensor (``lessons_period``), we only expose upcoming + lessons (starting from "now") and cap the number of lessons to avoid exceeding + Home Assistant recorder attribute size limits. + """ lessons = self.coordinator.data[self._key] attributes = [] canceled_counter = None @@ -299,6 +306,7 @@ def extra_state_attributes(self): "lessons_tomorrow", "lessons_next_day", ] + is_period = self._key == "lessons_period" lunch_break_time = datetime.strptime( self.coordinator.config_entry.options.get( "lunch_break_time", DEFAULT_LUNCH_BREAK_TIME @@ -312,11 +320,29 @@ def extra_state_attributes(self): self._lunch_break_start_at = None self._lunch_break_end_at = None canceled_counter = 0 - for lesson in lessons: - index = lessons.index(lesson) - if not ( - lesson.start == lessons[index - 1].start and lesson.canceled is True - ): + + # For the period timetable, keep only upcoming lessons and limit the count. + if is_period: + now = datetime.now() + filtered_lessons = [ + lesson for lesson in lessons if lesson.start >= now + ] + if len(filtered_lessons) > TIMETABLE_PERIOD_MAX_LESSONS: + filtered_lessons = filtered_lessons[:TIMETABLE_PERIOD_MAX_LESSONS] + else: + filtered_lessons = lessons + + for index, lesson in enumerate(filtered_lessons): + # Skip duplicated canceled lessons that share the same start time + # as the previous one. + if index > 0: + previous = filtered_lessons[index - 1] + if lesson.start == previous.start and lesson.canceled is True: + continue + + if is_period: + attributes.append(format_compact_lesson(lesson, lunch_break_time)) + else: attributes.append(format_lesson(lesson, lunch_break_time)) if lesson.canceled is False and self._start_at is None: self._start_at = lesson.start @@ -327,8 +353,8 @@ def extra_state_attributes(self): if lesson.end.time() < lunch_break_time: self._lunch_break_start_at = lesson.end if ( - self._lunch_break_end_at is None - and lesson.start.time() >= lunch_break_time + self._lunch_break_end_at is None + and lesson.start.time() >= lunch_break_time ): self._lunch_break_end_at = lesson.start @@ -666,4 +692,4 @@ def extra_state_attributes(self): attributes["periods"] = periods - return attributes + return attributes \ No newline at end of file