""" Timer Service fuer Phasen-Countdown. Berechnet verbleibende Zeit, Warnungen und Overtime-Status. """ from datetime import datetime from typing import Dict, Any from .models import LessonPhase, LessonSession class PhaseTimer: """ Timer Service fuer den Phasen-Countdown. Features: - Berechnet verbleibende Zeit pro Phase - Warnung bei 2 Minuten vor Ende - Overtime-Erkennung wenn Zeit abgelaufen """ # Warnung X Sekunden vor Ende (2 Minuten) WARNING_THRESHOLD_SECONDS = 120 def get_remaining_seconds(self, session: LessonSession) -> int: """ Berechnet die verbleibende Zeit in der aktuellen Phase. Beruecksichtigt Pause-Zeit: Wenn pausiert, wird die Zeit seit Pause-Start nicht mitgezaehlt. Die kumulative Pause-Zeit (total_paused_seconds) wird von der verstrichenen Zeit abgezogen. Args: session: Die aktuelle Session Returns: Verbleibende Sekunden (min 0) """ # Inaktive Phasen haben keinen Timer if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]: return 0 if not session.phase_started_at: return 0 # Phasendauer holen phase_id = session.current_phase.value duration_minutes = session.phase_durations.get(phase_id, 10) duration_seconds = duration_minutes * 60 # Verstrichene Zeit berechnen (mit Pause-Beruecksichtigung) elapsed = self._get_effective_elapsed(session) remaining = duration_seconds - elapsed return max(0, int(remaining)) def _get_effective_elapsed(self, session: LessonSession) -> float: """ Berechnet die effektive verstrichene Zeit abzueglich Pausenzeit. Args: session: Die aktuelle Session Returns: Effektive verstrichene Sekunden """ if not session.phase_started_at: return 0 # Basis: Zeit seit Phasenstart total_elapsed = (datetime.utcnow() - session.phase_started_at).total_seconds() # Abzug: Kumulative Pause-Zeit total_elapsed -= session.total_paused_seconds # Wenn aktuell pausiert: Zeit seit Pause-Start auch abziehen if session.is_paused and session.pause_started_at: current_pause = (datetime.utcnow() - session.pause_started_at).total_seconds() total_elapsed -= current_pause return max(0, total_elapsed) def get_elapsed_seconds(self, session: LessonSession) -> int: """ Berechnet die effektive verstrichene Zeit in der aktuellen Phase. Beruecksichtigt Pause-Zeit analog zu get_remaining_seconds. Args: session: Die aktuelle Session Returns: Verstrichene Sekunden (abzueglich Pausen) """ if not session.phase_started_at: return 0 if session.current_phase == LessonPhase.ENDED: return 0 return int(self._get_effective_elapsed(session)) def get_total_seconds(self, session: LessonSession) -> int: """ Gibt die Gesamtdauer der aktuellen Phase in Sekunden zurueck. Args: session: Die aktuelle Session Returns: Gesamtdauer in Sekunden """ if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]: return 0 phase_id = session.current_phase.value duration_minutes = session.phase_durations.get(phase_id, 10) return duration_minutes * 60 def get_percentage_remaining(self, session: LessonSession) -> int: """ Berechnet den Prozentsatz der verbleibenden Zeit. Args: session: Die aktuelle Session Returns: Prozent (0-100) """ total = self.get_total_seconds(session) if total == 0: return 0 remaining = self.get_remaining_seconds(session) return round((remaining / total) * 100) def get_percentage_elapsed(self, session: LessonSession) -> int: """ Berechnet den Prozentsatz der verstrichenen Zeit. Args: session: Die aktuelle Session Returns: Prozent (0-100) """ return 100 - self.get_percentage_remaining(session) def is_warning(self, session: LessonSession) -> bool: """ Prueft ob die Warnungszeit erreicht ist (2 Min vor Ende). Args: session: Die aktuelle Session Returns: True wenn Warnung aktiv """ if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]: return False remaining = self.get_remaining_seconds(session) return 0 < remaining <= self.WARNING_THRESHOLD_SECONDS def is_overtime(self, session: LessonSession) -> bool: """ Prueft ob die Phase ueberzogen wurde. Args: session: Die aktuelle Session Returns: True wenn Overtime """ if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]: return False remaining = self.get_remaining_seconds(session) return remaining == 0 def get_overtime_seconds(self, session: LessonSession) -> int: """ Berechnet wie viele Sekunden die Phase ueberzogen wurde. Args: session: Die aktuelle Session Returns: Overtime-Sekunden (0 wenn nicht ueberzogen) """ if not self.is_overtime(session): return 0 elapsed = self.get_elapsed_seconds(session) total = self.get_total_seconds(session) return max(0, elapsed - total) def format_time(self, seconds: int) -> str: """ Formatiert Sekunden als MM:SS String. Args: seconds: Sekunden Returns: Formatierte Zeit (z.B. "12:34") """ minutes = seconds // 60 secs = seconds % 60 return f"{minutes:02d}:{secs:02d}" def get_phase_status(self, session: LessonSession) -> Dict[str, Any]: """ Gibt den vollstaendigen Timer-Status zurueck. Args: session: Die aktuelle Session Returns: Dictionary mit Timer-Informationen """ remaining = self.get_remaining_seconds(session) total = self.get_total_seconds(session) elapsed = self.get_elapsed_seconds(session) overtime = self.get_overtime_seconds(session) return { "remaining_seconds": remaining, "remaining_formatted": self.format_time(remaining), "total_seconds": total, "total_formatted": self.format_time(total), "elapsed_seconds": elapsed, "elapsed_formatted": self.format_time(elapsed), "percentage_remaining": self.get_percentage_remaining(session), "percentage_elapsed": self.get_percentage_elapsed(session), "percentage": self.get_percentage_remaining(session), # Alias for Visual Timer "warning": self.is_warning(session), "overtime": self.is_overtime(session), "overtime_seconds": overtime, "overtime_formatted": self.format_time(overtime) if overtime > 0 else None, "is_paused": session.is_paused, } def get_lesson_timer(self, session: LessonSession) -> Dict[str, Any]: """ Gibt den Timer-Status fuer die gesamte Stunde zurueck. Args: session: Die aktuelle Session Returns: Dictionary mit Stunden-Timer-Informationen """ total_planned = session.get_total_duration_minutes() * 60 if session.lesson_started_at: if session.lesson_ended_at: actual_duration = (session.lesson_ended_at - session.lesson_started_at).total_seconds() else: actual_duration = (datetime.utcnow() - session.lesson_started_at).total_seconds() else: actual_duration = 0 return { "total_planned_seconds": total_planned, "total_planned_formatted": self.format_time(total_planned), "actual_duration_seconds": int(actual_duration), "actual_duration_formatted": self.format_time(int(actual_duration)), "started_at": session.lesson_started_at.isoformat() if session.lesson_started_at else None, "ended_at": session.lesson_ended_at.isoformat() if session.lesson_ended_at else None, }