"""A timeline is a set of events on a calendar. This timeline is used to iterate over program runtime events, managing the logic for interpreting recurring events for the Husqvarna Automower calendar. Only upcoming events will be shown. """ import datetime import logging from collections.abc import Iterable from dataclasses import dataclass from dateutil import rrule from ical.iter import ( LazySortableItem, RecurIterable, SortableItem, SortableItemTimeline, ) from ical.timespan import Timespan from .const import DayOfWeek, ProgramFrequency __all__ = ["ProgramEvent", "ProgramTimeline"] _LOGGER = logging.getLogger(__name__) RRULE_WEEKDAY = { DayOfWeek.MONDAY: rrule.MO, DayOfWeek.TUESDAY: rrule.TU, DayOfWeek.WEDNESDAY: rrule.WE, DayOfWeek.THURSDAY: rrule.TH, DayOfWeek.FRIDAY: rrule.FR, DayOfWeek.SATURDAY: rrule.SA, DayOfWeek.SUNDAY: rrule.SU, } @dataclass class ProgramEvent: """An instance of a program event.""" schedule_no: int work_area_id: int | None start: datetime.datetime end: datetime.datetime rule: rrule.rrule | None = None @property def rrule_str(self) -> str | None: """Return the recurrence rule string.""" rule_str = str(self.rule) if not self.rule or "DTSTART:" not in rule_str or "RRULE:" not in rule_str: return None parts = str(self.rule).split("\n") if len(parts) != 2: return None return parts[1].lstrip("RRULE:") # noqa: B005 class ProgramTimeline(SortableItemTimeline[ProgramEvent]): """A timeline of events in an irrigation program.""" def create_recurrence( *, schedule_no: int, work_area_id: int | None, frequency: ProgramFrequency, dtstart: datetime.datetime, duration: datetime.timedelta, days_of_week: set[DayOfWeek], ) -> Iterable[SortableItem[Timespan, ProgramEvent]]: """Create a timeline using a recurrence rule.""" byweekday = [RRULE_WEEKDAY[day_of_week] for day_of_week in days_of_week] ruleset = rrule.rruleset() rule: rrule.rrule if frequency == ProgramFrequency.DAILY: # Create a RRULE that is FREQ=DAILY rule = rrule.rrule( freq=rrule.DAILY, dtstart=dtstart, cache=True, ) elif frequency == ProgramFrequency.WEEKLY: # Create a RRULE that is FREQ=WEEKLY rule = rrule.rrule( freq=rrule.WEEKLY, byweekday=byweekday, dtstart=dtstart, cache=True, ) ruleset.rrule(rule) def adapter( dtstart: datetime.datetime | datetime.date, ) -> SortableItem[Timespan, ProgramEvent]: if not isinstance(dtstart, datetime.datetime): msg = "Expected datetime, got date" raise TypeError(msg) dtend = dtstart + duration def build() -> ProgramEvent: return ProgramEvent(schedule_no, work_area_id, dtstart, dtend, rule) return LazySortableItem(Timespan.of(dtstart, dtend), build) return RecurIterable(adapter, ruleset)