[1mdiff --git a/pyproject.toml b/pyproject.toml[m
[1mindex c0e9bdf..98c12c6 100644[m
[1m--- a/pyproject.toml[m
[1m+++ b/pyproject.toml[m
[36m@@ -75,8 +75,9 @@[m [minclude = ["src/**/*.py"][m
 [tool.ruff.format][m
 quote-style = "single"[m
 [m
[31m-[tool.ruff.lint.pydocstyle][m
[31m-convention = "google"[m
[32m+[m[32m[tool.ruff.lint][m
[32m+[m[32mextend-ignore = ["TRY003", "EM101"][m
[32m+[m[32mpydocstyle.convention = "google"[m
 [m
 [tool.mypy][m
 python_version = "3.10"[m
[1mdiff --git a/src/froeling/datamodels/component.py b/src/froeling/datamodels/component.py[m
[1mindex 55b6273..9920bb8 100644[m
[1m--- a/src/froeling/datamodels/component.py[m
[1m+++ b/src/froeling/datamodels/component.py[m
[36m@@ -5,7 +5,7 @@[m [mfrom http import HTTPStatus[m
 from typing import Any[m
 [m
 from froeling import endpoints[m
[31m-from froeling.datamodels.generics import TimeWindowDay[m
[32m+[m[32mfrom froeling.datamodels.timewindow import TimeWindowDay, TimeWindows[m
 from froeling.exceptions import NetworkError[m
 from froeling.session import Session[m
 [m
[36m@@ -31,10 +31,11 @@[m [mclass Component:[m
         standard_name (str | None): Standardized name, if available.[m
         type (str | None): Component type.[m
         sub_type (str | None): More specific component subtype.[m
[31m-        time_windows_view (list[TimeWindowDay] | None): Time window data, if fetched.[m
[32m+[m[32m        time_windows (TimeWindows | None): Time window data, if fetched.[m
         picture_url (str | None): URL to a representative image of the component.[m
         parameters (list[Parameter]): List of associated parameters.[m
[31m-        raw (dict)[m
[32m+[m[32m        time_windows_view (list[TimeWindowDay] | None): Compatibility shim for time_windows.[m
[32m+[m[32m        raw (dict): Raw data as received from the API.[m
 [m
     """[m
 [m
[36m@@ -45,7 +46,7 @@[m [mclass Component:[m
     standard_name: str | None[m
     type: str | None[m
     sub_type: str | None[m
[31m-    time_windows_view: list[TimeWindowDay] | None[m
[32m+[m[32m    time_windows: TimeWindows | None[m
     picture_url: str | None[m
 [m
     parameters: dict[str, 'Parameter'][m
[36m@@ -58,7 +59,7 @@[m [mclass Component:[m
         self.component_id = component_id[m
         self._session = session[m
 [m
[31m-        self.time_windows_view = None[m
[32m+[m[32m        self.time_windows = None[m
         self.picture_url = None[m
         self.parameters = {}[m
         self.raw = {}[m
[36m@@ -97,7 +98,7 @@[m [mclass Component:[m
         self.type = res.get('type')[m
         self.sub_type = res.get('subType')[m
         if res.get('timeWindowsView'):[m
[31m-            self.time_windows_view = TimeWindowDay._from_list(res['timeWindowsView'])  # noqa: SLF001[m
[32m+[m[32m            self.time_windows = TimeWindows._from_list(res['timeWindowsView'])  # noqa: SLF001[m
 [m
         #  TODO: Find endpoint that gives all parameters[m
         topview = res.get('topView')[m
[36m@@ -119,6 +120,11 @@[m [mclass Component:[m
         self.parameters = Parameter._from_list(list(parameters.values()), self._session, self.facility_id)  # noqa: SLF001[m
         return self.parameters[m
 [m
[32m+[m[32m    @property[m
[32m+[m[32m    def time_windows_view(self) -> list[TimeWindowDay] | None:[m
[32m+[m[32m        """Compatibility shim: old name for time_windows."""[m
[32m+[m[32m        return list(self.time_windows.values()) if self.time_windows else None[m
[32m+[m
 [m
 @dataclass[m
 class Parameter:[m
[1mdiff --git a/src/froeling/datamodels/facility.py b/src/froeling/datamodels/facility.py[m
[1mindex b2983e2..75151a4 100644[m
[1m--- a/src/froeling/datamodels/facility.py[m
[1m+++ b/src/froeling/datamodels/facility.py[m
[36m@@ -5,6 +5,7 @@[m [mfrom dataclasses import dataclass, field[m
 from froeling import endpoints[m
 from froeling.datamodels.component import Component[m
 from froeling.datamodels.generics import Address[m
[32m+[m[32mfrom froeling.datamodels.timewindow import TimeWindows[m
 from froeling.session import Session[m
 [m
 [m
[36m@@ -106,3 +107,16 @@[m [mclass Facility:[m
         Data will not be initialized, call the Component.update method to fetch them.[m
         """[m
         return Component(self.facility_id, component_id, self.session)[m
[32m+[m
[32m+[m[32m    async def update_time_windows(self, new_time_window: TimeWindows) -> dict:[m
[32m+[m[32m        """Update this Facility's time windows.[m
[32m+[m
[32m+[m[32m        Returns the raw response from the API. Example:[m
[32m+[m[32m        ```[m
[32m+[m[32m        {'updatedTimeWindowIds': [56]}[m
[32m+[m[32m        ```"""[m
[32m+[m[32m        return await self.session.request([m
[32m+[m[32m            'post',[m
[32m+[m[32m            endpoints.SET_FACILITY_TIME_WINDOWS.format(self.session.user_id, self.facility_id),[m
[32m+[m[32m            json=new_time_window.to_list(),[m
[32m+[m[32m        )[m
[1mdiff --git a/src/froeling/datamodels/generics.py b/src/froeling/datamodels/generics.py[m
[1mindex 4b9f5f6..27274c4 100644[m
[1m--- a/src/froeling/datamodels/generics.py[m
[1m+++ b/src/froeling/datamodels/generics.py[m
[36m@@ -1,7 +1,16 @@[m
 """Generic datamodels used in multiple places/endpoints."""[m
 [m
 from dataclasses import dataclass, field[m
[31m-from enum import Enum[m
[32m+[m
[32m+[m[32m# Backwards-compat re-exports for moved timewindow types[m
[32m+[m[32mfrom froeling.datamodels.timewindow import TimeWindowDay, TimeWindowPhase, TimeWindows, Weekday[m
[32m+[m
[32m+[m[32m__all__ = [[m
[32m+[m[32m    'Weekday',[m
[32m+[m[32m    'TimeWindows',[m
[32m+[m[32m    'TimeWindowDay',[m
[32m+[m[32m    'TimeWindowPhase',[m
[32m+[m[32m][m
 [m
 [m
 @dataclass(frozen=True)[m
[36m@@ -29,76 +38,3 @@[m [mclass Address:[m
         city = obj.get('city')[m
         country = obj.get('country')[m
         return Address(street, zipcode, city, country, obj)[m
[31m-[m
[31m-[m
[31m-class Weekday(Enum):[m
[31m-    """Enumeration of the days of the week."""[m
[31m-[m
[31m-    MONDAY = 'MONDAY'[m
[31m-    TUESDAY = 'TUESDAY'[m
[31m-    WEDNESDAY = 'WEDNESDAY'[m
[31m-    THURSDAY = 'THURSDAY'[m
[31m-    FRIDAY = 'FRIDAY'[m
[31m-    SATURDAY = 'SATURDAY'[m
[31m-    SUNDAY = 'SUNDAY'[m
[31m-[m
[31m-[m
[31m-@dataclass[m
[31m-class TimeWindowDay:[m
[31m-    """Represents the time window schedule for a single day of the week.[m
[31m-[m
[31m-    Attributes:[m
[31m-        id (int): Unique identifier for the day entry.[m
[31m-        weekday (Weekday): The day of the week.[m
[31m-        phases (list[TimeWindowPhase]): List of time phases for this day.[m
[31m-[m
[31m-    """[m
[31m-[m
[31m-    id: int[m
[31m-    weekday: Weekday[m
[31m-    phases: list['TimeWindowPhase'][m
[31m-    raw: dict = field(repr=False, default_factory=dict)[m
[31m-[m
[31m-    @classmethod[m
[31m-    def _from_dict(cls, obj: dict) -> 'TimeWindowDay':[m
[31m-        _id = obj['id'][m
[31m-        weekday = Weekday(obj['weekDay'])[m
[31m-        phases = TimeWindowPhase._from_list(obj['phases'])  # noqa: SLF001[m
[31m-[m
[31m-        return cls(_id, weekday, phases, obj)[m
[31m-[m
[31m-    @classmethod[m
[31m-    def _from_list(cls, obj: list) -> list['TimeWindowDay']:[m
[31m-        return [cls._from_dict(i) for i in obj][m
[31m-[m
[31m-[m
[31m-@dataclass[m
[31m-class TimeWindowPhase:[m
[31m-    """Represents a time phase within a single day.[m
[31m-[m
[31m-    Attributes:[m
[31m-        start_hour (int): Hour when the phase starts (0-23).[m
[31m-        start_minute (int): Minute when the phase starts (0-59).[m
[31m-        end_hour (int): Hour when the phase ends (0-23).[m
[31m-        end_minute (int): Minute when the phase ends (0-59).[m
[31m-[m
[31m-    """[m
[31m-[m
[31m-    start_hour: int[m
[31m-    start_minute: int[m
[31m-    end_hour: int[m
[31m-    end_minute: int[m
[31m-    raw: dict = field(repr=False, default_factory=dict)[m
[31m-[m
[31m-    @classmethod[m
[31m-    def _from_dict(cls, obj: dict) -> 'TimeWindowPhase':[m
[31m-        sh = obj['startHour'][m
[31m-        sm = obj['startMinute'][m
[31m-        eh = obj['endHour'][m
[31m-        em = obj['endMinute'][m
[31m-[m
[31m-        return cls(sh, sm, eh, em, obj)[m
[31m-[m
[31m-    @classmethod[m
[31m-    def _from_list(cls, obj: list) -> list['TimeWindowPhase']:[m
[31m-        return [cls._from_dict(i) for i in obj][m
[1mdiff --git a/src/froeling/datamodels/timewindow.py b/src/froeling/datamodels/timewindow.py[m
[1mnew file mode 100644[m
[1mindex 0000000..137b8f1[m
[1m--- /dev/null[m
[1m+++ b/src/froeling/datamodels/timewindow.py[m
[36m@@ -0,0 +1,241 @@[m
[32m+[m[32m"""Time window datamodels: Weekday, TimeWindowPhase, TimeWindowDay, TimeWindows.[m
[32m+[m
[32m+[m[32mThese are immutable value objects and provide helpers to convert to/from the[m
[32m+[m[32mAPI representation (lists/dicts with camelCase keys).[m
[32m+[m[32m"""[m
[32m+[m
[32m+[m[32mfrom collections.abc import Iterator[m
[32m+[m[32mfrom dataclasses import dataclass, field[m
[32m+[m[32mfrom enum import Enum[m
[32m+[m[32mfrom typing import Any[m
[32m+[m
[32m+[m
[32m+[m[32mclass Weekday(Enum):[m
[32m+[m[32m    """Enumeration of the days of the week."""[m
[32m+[m
[32m+[m[32m    MONDAY = 'MONDAY'[m
[32m+[m[32m    TUESDAY = 'TUESDAY'[m
[32m+[m[32m    WEDNESDAY = 'WEDNESDAY'[m
[32m+[m[32m    THURSDAY = 'THURSDAY'[m
[32m+[m[32m    FRIDAY = 'FRIDAY'[m
[32m+[m[32m    SATURDAY = 'SATURDAY'[m
[32m+[m[32m    SUNDAY = 'SUNDAY'[m
[32m+[m
[32m+[m
[32m+[m[32m@dataclass(frozen=True)[m
[32m+[m[32mclass TimeWindowPhase:[m
[32m+[m[32m    """Represents a time phase within a single day.[m
[32m+[m
[32m+[m[32m    Attributes:[m
[32m+[m[32m        start_hour (int): Hour when the phase starts (0-23).[m
[32m+[m[32m        start_minute (int): Minute when the phase starts (0-59).[m
[32m+[m[32m        end_hour (int): Hour when the phase ends (0-23).[m
[32m+[m[32m        end_minute (int): Minute when the phase ends (0-59).[m
[32m+[m[32m        raw (dict): Raw data as received from the API.[m
[32m+[m[32m    """[m
[32m+[m
[32m+[m[32m    start_hour: int[m
[32m+[m[32m    start_minute: int[m
[32m+[m[32m    end_hour: int[m
[32m+[m[32m    end_minute: int[m
[32m+[m[32m    raw: dict = field(repr=False, default_factory=dict)[m
[32m+[m
[32m+[m[32m    def to_dict(self) -> dict:[m
[32m+[m[32m        return {[m
[32m+[m[32m            'startHour': self.start_hour,[m
[32m+[m[32m            'startMinute': self.start_minute,[m
[32m+[m[32m            'endHour': self.end_hour,[m
[32m+[m[32m            'endMinute': self.end_minute,[m
[32m+[m[32m        }[m
[32m+[m
[32m+[m[32m    @classmethod[m
[32m+[m[32m    def from_hm(cls, sh: int, sm: int, eh: int, em: int) -> 'TimeWindowPhase':[m
[32m+[m[32m        """Create a TimeWindowPhase from hour/minute values with validation."""[m
[32m+[m[32m        cls._validate_bounds(sh, sm, eh, em)[m
[32m+[m[32m        return cls(sh, sm, eh, em)[m
[32m+[m
[32m+[m[32m    @staticmethod[m
[32m+[m[32m    def _validate_bounds(sh: int, sm: int, eh: int, em: int) -> None:[m
[32m+[m[32m        if not (0 <= sh <= 23 and 0 <= eh <= 23):  # noqa: PLR2004[m
[32m+[m[32m            raise ValueError('hour values must be between 0 and 23')[m
[32m+[m[32m        if not (0 <= sm <= 59 and 0 <= em <= 59):  # noqa: PLR2004[m
[32m+[m[32m            raise ValueError('minute values must be between 0 and 59')[m
[32m+[m[32m        if (eh, em) <= (sh, sm):[m
[32m+[m[32m            raise ValueError('end time must be after start time')[m
[32m+[m
[32m+[m[32m    def _overlap(self, b: 'TimeWindowPhase') -> bool:[m
[32m+[m[32m        """Return True if phase a overlaps phase b. Both are within same day."""[m
[32m+[m[32m        self_start = (self.start_hour, self.start_minute)[m
[32m+[m[32m        self_end = (self.end_hour, self.end_minute)[m
[32m+[m[32m        b_start = (b.start_hour, b.start_minute)[m
[32m+[m[32m        b_end = (b.end_hour, b.end_minute)[m
[32m+[m[32m        return (self_start < b_end) and (self_end > b_start)[m
[32m+[m
[32m+[m[32m    @classmethod[m
[32m+[m[32m    def _from_dict(cls, obj: dict) -> 'TimeWindowPhase':[m
[32m+[m[32m        sh = obj['startHour'][m
[32m+[m[32m        sm = obj['startMinute'][m
[32m+[m[32m        eh = obj['endHour'][m
[32m+[m[32m        em = obj['endMinute'][m
[32m+[m[32m        if not all(isinstance(v, int) for v in (sh, sm, eh, em)):[m
[32m+[m[32m            msg = 'TimeWindowPhase hour/minute values must be integers'[m
[32m+[m[32m            raise TypeError(msg)[m
[32m+[m[32m        cls._validate_bounds(sh, sm, eh, em)[m
[32m+[m[32m        return cls(sh, sm, eh, em, obj)[m
[32m+[m
[32m+[m[32m    @classmethod[m
[32m+[m[32m    def _from_list(cls, obj: list) -> list['TimeWindowPhase']:[m
[32m+[m[32m        return [cls._from_dict(i) for i in obj][m
[32m+[m
[32m+[m
[32m+[m[32m@dataclass(frozen=True)[m
[32m+[m[32mclass TimeWindowDay:[m
[32m+[m[32m    """Represents the time window schedule for a single day of the week.[m
[32m+[m
[32m+[m[32m    Attributes:[m
[32m+[m[32m        id (int): Unique identifier for the day entry.[m
[32m+[m[32m        weekday (Weekday): The day of the week.[m
[32m+[m[32m        phases (tuple[TimeWindowPhase,...]): Tuple of phases for this day.[m
[32m+[m[32m        raw (dict): Raw data as received from the API.[m
[32m+[m[32m    """[m
[32m+[m
[32m+[m[32m    id: int[m
[32m+[m[32m    weekday: Weekday[m
[32m+[m[32m    phases: tuple[TimeWindowPhase, ...][m
[32m+[m[32m    raw: dict = field(repr=False, default_factory=dict)[m
[32m+[m
[32m+[m[32m    @classmethod[m
[32m+[m[32m    def _from_dict(cls, obj: dict) -> 'TimeWindowDay':[m
[32m+[m[32m        _id = obj['id'][m
[32m+[m[32m        weekday = Weekday(obj['weekDay'])[m
[32m+[m[32m        phases = tuple(TimeWindowPhase._from_list(obj.get('phases', [])))  # noqa: SLF001[m
[32m+[m[32m        return cls(_id, weekday, phases, obj)[m
[32m+[m
[32m+[m[32m    @classmethod[m
[32m+[m[32m    def _from_list(cls, obj: list) -> list['TimeWindowDay']:[m
[32m+[m[32m        return [cls._from_dict(i) for i in obj][m
[32m+[m
[32m+[m[32m    def to_dict(self) -> dict:[m
[32m+[m[32m        return {[m
[32m+[m[32m            'id': self.id,[m
[32m+[m[32m            'weekDay': self.weekday.value if isinstance(self.weekday, Weekday) else str(self.weekday),[m
[32m+[m[32m            'phases': [p.to_dict() for p in self.phases],[m
[32m+[m[32m        }[m
[32m+[m
[32m+[m[32m    def add_phase(self, start_hour: int, start_minute: int, end_hour: int, end_minute: int) -> 'TimeWindowDay':[m
[32m+[m[32m        """Return a new TimeWindowDay with an additional phase added."""[m
[32m+[m[32m        TimeWindowPhase._validate_bounds(start_hour, start_minute, end_hour, end_minute)  # noqa: SLF001[m
[32m+[m[32m        new_phase = TimeWindowPhase(start_hour, start_minute, end_hour, end_minute)[m
[32m+[m[32m        for p in self.phases:[m
[32m+[m[32m            if p._overlap(new_phase):  # noqa: SLF001[m
[32m+[m[32m                raise ValueError('new phase overlaps existing phase')[m
[32m+[m[32m        new_phases = (*list(self.phases), new_phase)[m
[32m+[m[32m        return TimeWindowDay(self.id, self.weekday, new_phases, self.raw)[m
[32m+[m
[32m+[m[32m    def remove_phase(self, index: int) -> 'TimeWindowDay':[m
[32m+[m[32m        """Return a new TimeWindowDay with the phase at `index` removed."""[m
[32m+[m[32m        if not (0 <= index < len(self.phases)):[m
[32m+[m[32m            raise IndexError('phase index out of range')[m
[32m+[m[32m        new_phases = tuple(p for i, p in enumerate(self.phases) if i != index)[m
[32m+[m[32m        return TimeWindowDay(self.id, self.weekday, new_phases, self.raw)[m
[32m+[m
[32m+[m[32m    def replace_phase([m
[32m+[m[32m        self, index: int, start_hour: int, start_minute: int, end_hour: int, end_minute: int[m
[32m+[m[32m    ) -> 'TimeWindowDay':[m
[32m+[m[32m        """Return a new TimeWindowDay with the phase at `index` replaced by the provided values."""[m
[32m+[m[32m        if not (0 <= index < len(self.phases)):[m
[32m+[m[32m            raise IndexError('phase index out of range')[m
[32m+[m[32m        TimeWindowPhase._validate_bounds(start_hour, start_minute, end_hour, end_minute)  # noqa: SLF001[m
[32m+[m[32m        new_phase = TimeWindowPhase(start_hour, start_minute, end_hour, end_minute)[m
[32m+[m[32m        new_phases = list(self.phases)[m
[32m+[m[32m        new_phases[index] = new_phase[m
[32m+[m[32m        return TimeWindowDay(self.id, self.weekday, tuple(new_phases), self.raw)[m
[32m+[m
[32m+[m[32m    def replace_phases(self, new_phases: list[TimeWindowPhase]) -> 'TimeWindowDay':[m
[32m+[m[32m        """Return a new TimeWindowDay with the provided list of phases."""[m
[32m+[m[32m        for p in new_phases:[m
[32m+[m[32m            TimeWindowPhase._validate_bounds(p.start_hour, p.start_minute, p.end_hour, p.end_minute)  # noqa: SLF001[m
[32m+[m[32m        return TimeWindowDay(self.id, self.weekday, tuple(new_phases), self.raw)[m
[32m+[m
[32m+[m
[32m+[m[32m@dataclass(frozen=True)[m
[32m+[m[32mclass TimeWindows:[m
[32m+[m[32m    """Wrapper around multiple `TimeWindowDay` entries.[m
[32m+[m
[32m+[m[32m    This behaves like a dict and provides helpers to[m
[32m+[m[32m    convert to/from the API representation (a JSON array of day objects).[m
[32m+[m[32m    You can only access days by their `Weekday` enum value.[m
[32m+[m[32m    """[m
[32m+[m
[32m+[m[32m    days: tuple[TimeWindowDay, ...][m
[32m+[m
[32m+[m[32m    @classmethod[m
[32m+[m[32m    def _from_list(cls, obj: list) -> 'TimeWindows':[m
[32m+[m[32m        days = {d.weekday: d for d in TimeWindowDay._from_list(obj)}  # noqa: SLF001[m
[32m+[m[32m        if len(days) != 7:  # noqa: PLR2004[m
[32m+[m[32m            raise ValueError('TimeWindows must contain exactly one entry for each weekday')[m
[32m+[m[32m        return cls(tuple(days.values()))[m
[32m+[m
[32m+[m[32m    def to_list(self) -> list[dict]:[m
[32m+[m[32m        return [d.to_dict() for d in self.days][m
[32m+[m
[32m+[m[32m    def __iter__(self) -> Iterator[TimeWindowDay]:[m
[32m+[m[32m        return iter(self.days)[m
[32m+[m
[32m+[m[32m    def __len__(self) -> int:[m
[32m+[m[32m        return len(self.days)[m
[32m+[m
[32m+[m[32m    def __getitem__(self, key: Weekday) -> TimeWindowDay:[m
[32m+[m[32m        """Allow lookup by `Weekday`."""[m
[32m+[m[32m        for d in self.days:[m
[32m+[m[32m            if d.weekday == key:[m
[32m+[m[32m                return d[m
[32m+[m[32m        msg = f'TimeWindows has no entry for weekday {key}'[m
[32m+[m[32m        raise KeyError(msg)[m
[32m+[m
[32m+[m[32m    def get(self, key: Weekday, default: Any = None) -> TimeWindowDay | None:[m
[32m+[m[32m        """Return the `TimeWindowDay` for `weekday`. Always returns a day (may be a default one)."""[m
[32m+[m[32m        try:[m
[32m+[m[32m            return self[key][m
[32m+[m[32m        except KeyError:[m
[32m+[m[32m            return default[m
[32m+[m
[32m+[m[32m    def keys(self) -> list[Weekday]:[m
[32m+[m[32m        return [d.weekday for d in self.days][m
[32m+[m
[32m+[m[32m    def values(self) -> list[TimeWindowDay]:[m
[32m+[m[32m        return list(self.days)[m
[32m+[m
[32m+[m[32m    def items(self) -> list[tuple[Weekday, TimeWindowDay]]:[m
[32m+[m[32m        return [(d.weekday, d) for d in self.days][m
[32m+[m
[32m+[m[32m    def __contains__(self, key: Weekday) -> bool:[m
[32m+[m[32m        return any(d.weekday == key for d in self.days)[m
[32m+[m
[32m+[m[32m    @property[m
[32m+[m[32m    def monday(self) -> TimeWindowDay:[m
[32m+[m[32m        return self[Weekday.MONDAY][m
[32m+[m
[32m+[m[32m    @property[m
[32m+[m[32m    def tuesday(self) -> TimeWindowDay:[m
[32m+[m[32m        return self[Weekday.TUESDAY][m
[32m+[m
[32m+[m[32m    @property[m
[32m+[m[32m    def wednesday(self) -> TimeWindowDay:[m
[32m+[m[32m        return self[Weekday.WEDNESDAY][m
[32m+[m
[32m+[m[32m    @property[m
[32m+[m[32m    def thursday(self) -> TimeWindowDay:[m
[32m+[m[32m        return self[Weekday.THURSDAY][m
[32m+[m
[32m+[m[32m    @property[m
[32m+[m[32m    def friday(self) -> TimeWindowDay:[m
[32m+[m[32m        return self[Weekday.FRIDAY][m
[32m+[m
[32m+[m[32m    @property[m
[32m+[m[32m    def saturday(self) -> TimeWindowDay:[m
[32m+[m[32m        return self[Weekday.SATURDAY][m
[32m+[m
[32m+[m[32m    @property[m
[32m+[m[32m    def sunday(self) -> TimeWindowDay:[m
[32m+[m[32m        return self[Weekday.SUNDAY][m
[1mdiff --git a/src/froeling/endpoints.py b/src/froeling/endpoints.py[m
[1mindex 2dec52c..8bcea11 100644[m
[1m--- a/src/froeling/endpoints.py[m
[1m+++ b/src/froeling/endpoints.py[m
[36m@@ -30,3 +30,6 @@[m [mNOTIFICATION = 'https://connect-api.froeling.com/connect/v1.0/resources/service/[m
 [m
 SET_PARAMETER = 'https://connect-api.froeling.com/fcs/v1.0/resources/user/{}/facility/{}/parameter/{}'[m
 """1: user_id  2: facility_id  3: parameter_id"""[m
[32m+[m
[32m+[m[32mSET_FACILITY_TIME_WINDOWS = 'https://connect-api.froeling.com/fcs/v1.0/resources/user/{}/facility/{}/timeWindows'[m
[32m+[m[32m"""1: user_id  2: facility_id"""[m
[1mdiff --git a/tests/test_facility.py b/tests/test_facility.py[m
[1mindex d1ac475..5d803d7 100644[m
[1m--- a/tests/test_facility.py[m
[1m+++ b/tests/test_facility.py[m
[36m@@ -229,7 +229,7 @@[m [masync def test_facility_get_components(load_json, component_id, expected):[m
 [m
             for field, value in expected.items():[m
                 assert getattr(comp, field) == value[m
[31m-                assert comp.time_windows_view is None[m
[32m+[m[32m                assert comp.time_windows is None[m
                 assert comp.picture_url is None[m
                 assert comp.parameters == {}[m
 [m
[1mdiff --git a/tests/test_timewindow.py b/tests/test_timewindow.py[m
[1mnew file mode 100644[m
[1mindex 0000000..8d819e6[m
[1m--- /dev/null[m
[1m+++ b/tests/test_timewindow.py[m
[36m@@ -0,0 +1,94 @@[m
[32m+[m[32mimport pytest[m
[32m+[m[32mfrom aioresponses import aioresponses[m
[32m+[m
[32m+[m[32mfrom froeling import Froeling, endpoints[m
[32m+[m[32mfrom froeling.datamodels.timewindow import ([m
[32m+[m[32m    TimeWindows,[m
[32m+[m[32m    TimeWindowDay,[m
[32m+[m[32m    TimeWindowPhase,[m
[32m+[m[32m    Weekday,[m
[32m+[m[32m)[m
[32m+[m
[32m+[m
[32m+[m[32m@pytest.mark.asyncio[m
[32m+[m[32masync def test_update_time_windows_posts_payload(load_json):[m
[32m+[m[32m    payload = [[m
[32m+[m[32m        {"id": 56, "weekDay": "MONDAY", "phases": [{"startHour": 6, "startMinute": 10, "endHour": 20, "endMinute": 0}]},[m
[32m+[m[32m        {"id": 57, "weekDay": "TUESDAY", "phases": [{"startHour": 6, "startMinute": 10, "endHour": 20, "endMinute": 0}]},[m
[32m+[m[32m        {"id": 58, "weekDay": "WEDNESDAY", "phases": [{"startHour": 6, "startMinute": 10, "endHour": 20, "endMinute": 0}]},[m
[32m+[m[32m        {"id": 59, "weekDay": "THURSDAY", "phases": [{"startHour": 6, "startMinute": 10, "endHour": 20, "endMinute": 0}]},[m
[32m+[m[32m        {"id": 60, "weekDay": "FRIDAY", "phases": [{"startHour": 6, "startMinute": 10, "endHour": 19, "endMinute": 0}]},[m
[32m+[m[32m        {"id": 61, "weekDay": "SATURDAY", "phases": [{"startHour": 7, "startMinute": 0, "endHour": 16, "endMinute": 0}]},[m
[32m+[m[32m        {"id": 62, "weekDay": "SUNDAY", "phases": [{"startHour": 9, "startMinute": 0, "endHour": 16, "endMinute": 0}]},[m
[32m+[m[32m    ][m
[32m+[m
[32m+[m[32m    facility_data = load_json('facility.json')[m
[32m+[m
[32m+[m[32m    token = 'header.eyJ1c2VySWQiOjEyMzR9.signature'[m
[32m+[m
[32m+[m[32m    with aioresponses() as m:[m
[32m+[m[32m        m.get(endpoints.FACILITY.format(1234), status=200, payload=facility_data)[m
[32m+[m[32m        m.post([m
[32m+[m[32m            endpoints.SET_FACILITY_TIME_WINDOWS.format(1234, 12345),[m
[32m+[m[32m            status=200,[m
[32m+[m[32m            payload={"updatedTimeWindowIds": [56]},[m
[32m+[m[32m        )[m
[32m+[m
[32m+[m[32m        async with Froeling(token=token) as api:[m
[32m+[m[32m            f = await api.get_facility(12345)[m
[32m+[m
[32m+[m[32m            tw = TimeWindows._from_list(payload)[m
[32m+[m[32m            res = await f.update_time_windows(tw)[m
[32m+[m
[32m+[m[32m            assert res == {"updatedTimeWindowIds": [56]}[m
[32m+[m
[32m+[m
[32m+[m[32mdef test_timewindow_phase_validation_and_to_dict():[m
[32m+[m[32m    p = TimeWindowPhase.from_hm(6, 10, 20, 0)[m
[32m+[m[32m    assert p.to_dict() == {"startHour": 6, "startMinute": 10, "endHour": 20, "endMinute": 0}[m
[32m+[m
[32m+[m[32m    with pytest.raises(ValueError):[m
[32m+[m[32m        TimeWindowPhase.from_hm(24, 0, 1, 0)[m
[32m+[m
[32m+[m[32m    with pytest.raises(ValueError):[m
[32m+[m[32m        TimeWindowPhase.from_hm(10, 0, 9, 59)[m
[32m+[m
[32m+[m
[32m+[m[32mdef test_timewindow_phase_overlap():[m
[32m+[m[32m    a = TimeWindowPhase.from_hm(6, 0, 8, 0)[m
[32m+[m[32m    b = TimeWindowPhase.from_hm(7, 30, 9, 0)[m
[32m+[m[32m    c = TimeWindowPhase.from_hm(8, 0, 10, 0)[m
[32m+[m
[32m+[m[32m    assert a._overlap(b) is True[m
[32m+[m[32m    assert a._overlap(c) is False[m
[32m+[m
[32m+[m
[32m+[m[32mdef test_timewindow_day_modifications():[m
[32m+[m[32m    day_dict = {"id": 1, "weekDay": "MONDAY", "phases": [{"startHour": 6, "startMinute": 0, "endHour": 8, "endMinute": 0}]}[m
[32m+[m[32m    day = TimeWindowDay._from_dict(day_dict)[m
[32m+[m
[32m+[m[32m    with pytest.raises(ValueError):[m
[32m+[m[32m        day.add_phase(7, 0, 9, 0)[m
[32m+[m
[32m+[m[32m    new_day = day.add_phase(9, 0, 10, 0)[m
[32m+[m[32m    assert len(new_day.phases) == 2[m
[32m+[m
[32m+[m[32m    replaced = new_day.replace_phase(1, 10, 0, 11, 0)[m
[32m+[m[32m    assert replaced.phases[1].start_hour == 10[m
[32m+[m
[32m+[m[32m    removed = replaced.remove_phase(1)[m
[32m+[m[32m    assert len(removed.phases) == 1[m
[32m+[m
[32m+[m
[32m+[m[32mdef test_timewindow_wrapper_keys_and_props():[m
[32m+[m[32m    week = [][m
[32m+[m[32m    base_id = 56[m
[32m+[m[32m    hours = [6, 6, 6, 6, 6, 7, 9][m
[32m+[m[32m    weekdays = [w.value for w in Weekday][m
[32m+[m[32m    for i, wd in enumerate(weekdays):[m
[32m+[m[32m        week.append({"id": base_id + i, "weekDay": wd, "phases": [{"startHour": hours[i], "startMinute": 0, "endHour": 16 if i >= 5 else 20, "endMinute": 0}]})[m
[32m+[m
[32m+[m[32m    tw = TimeWindows._from_list(week)[m
[32m+[m[32m    assert len(tw) == 7[m
[32m+[m[32m    assert Weekday.MONDAY in tw.keys()[m
[32m+[m[32m    assert tw.monday.weekday == Weekday.MONDAY[m
