From 69a5ff935c8d76fad76a6fd74d779b39dc2b293a Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 21 Feb 2023 01:45:25 +0100 Subject: [PATCH] Remove {Light,Vacuum}Interfaces (#1743) This PR removes `LightInterface` (which was never part of a release) and `VacuumInterface` (which was mostly used to enforce the method naming inside the library). --- miio/__init__.py | 1 - miio/identifiers.py | 19 ++++- .../dreame/vacuum/dreamevacuum_miot.py | 5 +- miio/integrations/ijai/vacuum/pro2vacuum.py | 5 +- miio/integrations/mijia/vacuum/g1vacuum.py | 6 +- miio/integrations/roborock/vacuum/vacuum.py | 7 +- .../roborock/vacuum/vacuumcontainers.py | 5 +- .../roidmi/vacuum/roidmivacuum_miot.py | 6 +- miio/integrations/viomi/vacuum/viomivacuum.py | 12 ++- .../yeelight/light/spec_helper.py | 8 +- .../light/tests/test_yeelight_spec_helper.py | 20 +++-- miio/integrations/yeelight/light/yeelight.py | 12 ++- miio/interfaces/__init__.py | 11 --- miio/interfaces/lightinterface.py | 39 --------- miio/interfaces/vacuuminterface.py | 80 ------------------- miio/tests/test_vacuums.py | 79 ------------------ 16 files changed, 56 insertions(+), 259 deletions(-) delete mode 100644 miio/interfaces/__init__.py delete mode 100644 miio/interfaces/lightinterface.py delete mode 100644 miio/interfaces/vacuuminterface.py delete mode 100644 miio/tests/test_vacuums.py diff --git a/miio/__init__.py b/miio/__init__.py index 8416588ce..1a38069da 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -12,7 +12,6 @@ from miio.exceptions import DeviceError, DeviceException, UnsupportedFeatureException from miio.miot_device import MiotDevice from miio.deviceinfo import DeviceInfo -from miio.interfaces import VacuumInterface, LightInterface, ColorTemperatureRange # isort: on diff --git a/miio/identifiers.py b/miio/identifiers.py index d1bd11b50..d7592250c 100644 --- a/miio/identifiers.py +++ b/miio/identifiers.py @@ -1,5 +1,5 @@ """Compat layer for homeassistant.""" -from enum import Enum +from enum import Enum, auto class StandardIdentifier(Enum): @@ -48,3 +48,20 @@ class LightId(StandardIdentifier): Brightness = "light:brightness" ColorTemperature = "light:color-temperature" Color = "light:color" + + +class VacuumState(Enum): + """Vacuum state enum. + + This offers a simplified API to the vacuum state. + + # TODO: the interpretation of simplified state should be done downstream. + """ + + Unknown = auto() + Cleaning = auto() + Returning = auto() + Idle = auto() + Docked = auto() + Paused = auto() + Error = auto() diff --git a/miio/integrations/dreame/vacuum/dreamevacuum_miot.py b/miio/integrations/dreame/vacuum/dreamevacuum_miot.py index b94ddc6b8..801f8bc64 100644 --- a/miio/integrations/dreame/vacuum/dreamevacuum_miot.py +++ b/miio/integrations/dreame/vacuum/dreamevacuum_miot.py @@ -8,7 +8,6 @@ import click from miio.click_common import command, format_output -from miio.interfaces import FanspeedPresets, VacuumInterface from miio.miot_device import DeviceStatus as DeviceStatusContainer from miio.miot_device import MiotDevice, MiotMapping from miio.updater import OneShotServer @@ -464,7 +463,7 @@ def is_water_box_carriage_attached(self) -> Optional[bool]: return None -class DreameVacuum(MiotDevice, VacuumInterface): +class DreameVacuum(MiotDevice): _mappings = MIOT_MAPPING @command( @@ -588,7 +587,7 @@ def set_fan_speed(self, speed: int): return self.set_property("cleaning_mode", fanspeed.value) @command() - def fan_speed_presets(self) -> FanspeedPresets: + def fan_speed_presets(self) -> Dict[str, int]: """Return available fan speed presets.""" fanspeeds_enum = _get_cleaning_mode_enum_class(self.model) if not fanspeeds_enum: diff --git a/miio/integrations/ijai/vacuum/pro2vacuum.py b/miio/integrations/ijai/vacuum/pro2vacuum.py index b989c96ed..6f35e3a5e 100644 --- a/miio/integrations/ijai/vacuum/pro2vacuum.py +++ b/miio/integrations/ijai/vacuum/pro2vacuum.py @@ -7,7 +7,6 @@ from miio.click_common import EnumType, command, format_output from miio.devicestatus import sensor, setting -from miio.interfaces import FanspeedPresets, VacuumInterface from miio.miot_device import DeviceStatus, MiotDevice _LOGGER = logging.getLogger(__name__) @@ -267,7 +266,7 @@ def current_language(self) -> str: return self.data["current_language"] -class Pro2Vacuum(MiotDevice, VacuumInterface): +class Pro2Vacuum(MiotDevice): """Support for Mi Robot Vacuum-Mop 2 Pro (ijai.vacuum.v3).""" _mappings = _MAPPINGS @@ -308,7 +307,7 @@ def set_fan_speed(self, fan_speed: FanSpeedMode): return self.set_property("fan_speed", fan_speed) @command() - def fan_speed_presets(self) -> FanspeedPresets: + def fan_speed_presets(self) -> Dict[str, int]: """Return available fan speed presets.""" return _enum_as_dict(FanSpeedMode) diff --git a/miio/integrations/mijia/vacuum/g1vacuum.py b/miio/integrations/mijia/vacuum/g1vacuum.py index d042e11d8..38eeea4bb 100644 --- a/miio/integrations/mijia/vacuum/g1vacuum.py +++ b/miio/integrations/mijia/vacuum/g1vacuum.py @@ -1,11 +1,11 @@ import logging from datetime import timedelta from enum import Enum +from typing import Dict import click from miio.click_common import EnumType, command, format_output -from miio.interfaces import FanspeedPresets, VacuumInterface from miio.miot_device import DeviceStatus, MiotDevice _LOGGER = logging.getLogger(__name__) @@ -279,7 +279,7 @@ def total_clean_time(self) -> timedelta: return timedelta(hours=self.data["total_clean_area"]) -class G1Vacuum(MiotDevice, VacuumInterface): +class G1Vacuum(MiotDevice): """Support for G1 vacuum (G1, mijia.vacuum.v2).""" _mappings = MIOT_MAPPING @@ -379,7 +379,7 @@ def set_fan_speed(self, fan_speed: G1FanSpeed): return self.set_property("fan_speed", fan_speed.value) @command() - def fan_speed_presets(self) -> FanspeedPresets: + def fan_speed_presets(self) -> Dict[str, int]: """Return available fan speed presets.""" return {x.name: x.value for x in G1FanSpeed} diff --git a/miio/integrations/roborock/vacuum/vacuum.py b/miio/integrations/roborock/vacuum/vacuum.py index 1486d449e..50384bf35 100644 --- a/miio/integrations/roborock/vacuum/vacuum.py +++ b/miio/integrations/roborock/vacuum/vacuum.py @@ -8,7 +8,7 @@ import pathlib import time from enum import Enum -from typing import Any, List, Optional, Type +from typing import Any, Dict, List, Optional, Type import click import pytz @@ -25,7 +25,6 @@ from miio.devicestatus import DeviceStatus, action from miio.exceptions import DeviceInfoUnavailableException, UnsupportedFeatureException from miio.identifiers import VacuumId -from miio.interfaces import FanspeedPresets, VacuumInterface from .updatehelper import UpdateHelper from .vacuum_enums import ( @@ -123,7 +122,7 @@ ] -class RoborockVacuum(Device, VacuumInterface): +class RoborockVacuum(Device): """Main class for roborock vacuums (roborock.vacuum.*).""" _supported_models = SUPPORTED_MODELS @@ -649,7 +648,7 @@ def fan_speed(self): return self.send("get_custom_mode")[0] @command() - def fan_speed_presets(self) -> FanspeedPresets: + def fan_speed_presets(self) -> Dict[str, int]: """Return available fan speed presets.""" def _enum_as_dict(cls): diff --git a/miio/integrations/roborock/vacuum/vacuumcontainers.py b/miio/integrations/roborock/vacuum/vacuumcontainers.py index f03df06f0..ef52a0dff 100644 --- a/miio/integrations/roborock/vacuum/vacuumcontainers.py +++ b/miio/integrations/roborock/vacuum/vacuumcontainers.py @@ -8,8 +8,7 @@ from miio.device import DeviceStatus from miio.devicestatus import sensor, setting -from miio.identifiers import VacuumId -from miio.interfaces.vacuuminterface import VacuumDeviceStatus, VacuumState +from miio.identifiers import VacuumId, VacuumState from miio.utils import pretty_seconds, pretty_time from .vacuum_enums import MopIntensity, MopMode @@ -134,7 +133,7 @@ def map_name_dict(self) -> Dict[str, int]: return self._map_name_dict -class VacuumStatus(VacuumDeviceStatus): +class VacuumStatus(DeviceStatus): """Container for status reports from the vacuum.""" def __init__(self, data: Dict[str, Any]) -> None: diff --git a/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py b/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py index 71df21f75..7330fd166 100644 --- a/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py +++ b/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py @@ -6,6 +6,7 @@ import math from datetime import timedelta from enum import Enum +from typing import Dict import click @@ -13,7 +14,6 @@ from miio.integrations.roborock.vacuum.vacuumcontainers import ( # TODO: remove roborock import DNDStatus, ) -from miio.interfaces import FanspeedPresets, VacuumInterface from miio.miot_device import DeviceStatus, MiotDevice, MiotMapping _LOGGER = logging.getLogger(__name__) @@ -548,7 +548,7 @@ def sensor_dirty_left(self) -> timedelta: return timedelta(minutes=self.data["sensor_dirty_time_left_minutes"]) -class RoidmiVacuumMiot(MiotDevice, VacuumInterface): +class RoidmiVacuumMiot(MiotDevice): """Interface for Vacuum Eve Plus (roidmi.vacuum.v60)""" _mappings = _MAPPINGS @@ -651,7 +651,7 @@ def set_fanspeed(self, fanspeed_mode: FanSpeed): return self.set_property("fanspeed_mode", fanspeed_mode.value) @command() - def fan_speed_presets(self) -> FanspeedPresets: + def fan_speed_presets(self) -> Dict[str, int]: """Return available fan speed presets.""" return {"Sweep": 0, "Silent": 1, "Basic": 2, "Strong": 3, "FullSpeed": 4} diff --git a/miio/integrations/viomi/vacuum/viomivacuum.py b/miio/integrations/viomi/vacuum/viomivacuum.py index 009b4a454..327ec6754 100644 --- a/miio/integrations/viomi/vacuum/viomivacuum.py +++ b/miio/integrations/viomi/vacuum/viomivacuum.py @@ -53,15 +53,13 @@ from miio.click_common import EnumType, command from miio.device import Device -from miio.devicestatus import action, sensor, setting +from miio.devicestatus import DeviceStatus, action, sensor, setting from miio.exceptions import DeviceException -from miio.identifiers import VacuumId +from miio.identifiers import VacuumId, VacuumState from miio.integrations.roborock.vacuum.vacuumcontainers import ( # TODO: remove roborock import ConsumableStatus, DNDStatus, ) -from miio.interfaces import FanspeedPresets, VacuumInterface -from miio.interfaces.vacuuminterface import VacuumDeviceStatus, VacuumState from miio.utils import pretty_seconds _LOGGER = logging.getLogger(__name__) @@ -270,7 +268,7 @@ class ViomiEdgeState(Enum): Unknown2 = 5 -class ViomiVacuumStatus(VacuumDeviceStatus): +class ViomiVacuumStatus(DeviceStatus): def __init__(self, data): """Vacuum status container. @@ -582,7 +580,7 @@ def _get_rooms_from_schedules(schedules: List[str]) -> Tuple[bool, Dict]: return scheduled_found, rooms -class ViomiVacuum(Device, VacuumInterface): +class ViomiVacuum(Device): """Interface for Viomi vacuums (viomi.vacuum.v7).""" _supported_models = SUPPORTED_MODELS @@ -794,7 +792,7 @@ def set_fan_speed(self, speed: ViomiVacuumSpeed): self.send("set_suction", [speed.value]) @command() - def fan_speed_presets(self) -> FanspeedPresets: + def fan_speed_presets(self) -> Dict[str, int]: """Return available fan speed presets.""" return {x.name: x.value for x in list(ViomiVacuumSpeed)} diff --git a/miio/integrations/yeelight/light/spec_helper.py b/miio/integrations/yeelight/light/spec_helper.py index aa1ac796c..7bd618bdf 100644 --- a/miio/integrations/yeelight/light/spec_helper.py +++ b/miio/integrations/yeelight/light/spec_helper.py @@ -6,7 +6,7 @@ import attr import yaml -from miio import ColorTemperatureRange +from miio.descriptors import ValidSettingRange _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,7 @@ class YeelightSubLightType(IntEnum): @attr.s(auto_attribs=True) class YeelightLampInfo: - color_temp: ColorTemperatureRange + color_temp: ValidSettingRange supports_color: bool @@ -43,14 +43,14 @@ def _parse_specs_yaml(self): for key, value in models.items(): lamps = { YeelightSubLightType.Main: YeelightLampInfo( - ColorTemperatureRange(*value["color_temp"]), + ValidSettingRange(*value["color_temp"]), value["supports_color"], ) } if "background" in value: lamps[YeelightSubLightType.Background] = YeelightLampInfo( - ColorTemperatureRange(*value["background"]["color_temp"]), + ValidSettingRange(*value["background"]["color_temp"]), value["background"]["supports_color"], ) diff --git a/miio/integrations/yeelight/light/tests/test_yeelight_spec_helper.py b/miio/integrations/yeelight/light/tests/test_yeelight_spec_helper.py index 765fa3c6c..ff0a2f120 100644 --- a/miio/integrations/yeelight/light/tests/test_yeelight_spec_helper.py +++ b/miio/integrations/yeelight/light/tests/test_yeelight_spec_helper.py @@ -1,8 +1,6 @@ -from ..spec_helper import ( - ColorTemperatureRange, - YeelightSpecHelper, - YeelightSubLightType, -) +from miio.descriptors import ValidSettingRange + +from ..spec_helper import YeelightSpecHelper, YeelightSubLightType def test_get_model_info(): @@ -10,9 +8,9 @@ def test_get_model_info(): model_info = spec_helper.get_model_info("yeelink.light.bslamp1") assert model_info.model == "yeelink.light.bslamp1" assert model_info.night_light is False - assert model_info.lamps[ - YeelightSubLightType.Main - ].color_temp == ColorTemperatureRange(1700, 6500) + assert model_info.lamps[YeelightSubLightType.Main].color_temp == ValidSettingRange( + 1700, 6500 + ) assert model_info.lamps[YeelightSubLightType.Main].supports_color is True assert YeelightSubLightType.Background not in model_info.lamps @@ -22,8 +20,8 @@ def test_get_unknown_model_info(): model_info = spec_helper.get_model_info("notreal") assert model_info.model == "yeelink.light.*" assert model_info.night_light is False - assert model_info.lamps[ - YeelightSubLightType.Main - ].color_temp == ColorTemperatureRange(1700, 6500) + assert model_info.lamps[YeelightSubLightType.Main].color_temp == ValidSettingRange( + 1700, 6500 + ) assert model_info.lamps[YeelightSubLightType.Main].supports_color is False assert YeelightSubLightType.Background not in model_info.lamps diff --git a/miio/integrations/yeelight/light/yeelight.py b/miio/integrations/yeelight/light/yeelight.py index e910c678d..bc2f4d7ea 100644 --- a/miio/integrations/yeelight/light/yeelight.py +++ b/miio/integrations/yeelight/light/yeelight.py @@ -4,7 +4,6 @@ import click -from miio import LightInterface from miio.click_common import command, format_output from miio.descriptors import ( NumberSettingDescriptor, @@ -283,7 +282,7 @@ def lights(self) -> List[YeelightSubLight]: return sub_lights -class Yeelight(Device, LightInterface): +class Yeelight(Device): """A rudimentary support for Yeelight bulbs. The API is the same as defined in @@ -364,8 +363,8 @@ def settings(self) -> Dict[str, SettingDescriptor]: # TODO: unclear semantics on settings, as making changes here will affect other instances of the class... settings = super().settings().copy() ct = self._light_info.color_temp - if ct.min != ct.max: - _LOGGER.info("Got ct for %s: %s", self.model, ct) + if ct.min_value != ct.max_value: + _LOGGER.debug("Got ct for %s: %s", self.model, ct) settings[LightId.ColorTemperature.value] = NumberSettingDescriptor( name="Color temperature", id=LightId.ColorTemperature.value, @@ -377,7 +376,7 @@ def settings(self) -> Dict[str, SettingDescriptor]: unit="kelvin", ) if self._light_info.supports_color: - _LOGGER.info("Got color for %s", self.model) + _LOGGER.debug("Got color for %s", self.model) settings[LightId.Color.value] = NumberSettingDescriptor( name="Color", id=LightId.Color.value, @@ -393,8 +392,7 @@ def settings(self) -> Dict[str, SettingDescriptor]: @property def color_temperature_range(self) -> ValidSettingRange: """Return supported color temperature range.""" - temps = self._light_info.color_temp - return ValidSettingRange(min_value=temps[0], max_value=temps[1]) + return self._light_info.color_temp @command( click.option("--transition", type=int, required=False, default=0), diff --git a/miio/interfaces/__init__.py b/miio/interfaces/__init__.py deleted file mode 100644 index 4f0247c41..000000000 --- a/miio/interfaces/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Interfaces API.""" - -from .lightinterface import ColorTemperatureRange, LightInterface -from .vacuuminterface import FanspeedPresets, VacuumInterface - -__all__ = [ - "FanspeedPresets", - "VacuumInterface", - "LightInterface", - "ColorTemperatureRange", -] diff --git a/miio/interfaces/lightinterface.py b/miio/interfaces/lightinterface.py deleted file mode 100644 index 40d338b69..000000000 --- a/miio/interfaces/lightinterface.py +++ /dev/null @@ -1,39 +0,0 @@ -"""`LightInterface` is an interface (abstract class) for light devices.""" -from abc import abstractmethod -from typing import NamedTuple, Optional, Tuple - -from miio.descriptors import ValidSettingRange - - -class ColorTemperatureRange(NamedTuple): - """Color temperature range.""" - - min: int - max: int - - -class LightInterface: - """Light interface.""" - - @abstractmethod - def set_power(self, on: bool, **kwargs): - """Turn device on or off.""" - - @abstractmethod - def set_brightness(self, level: int, **kwargs): - """Set the light brightness [0,100].""" - - @property - def color_temperature_range(self) -> Optional[ValidSettingRange]: - """Return the color temperature range, if supported.""" - return None - - def set_color_temperature(self, level: int, **kwargs): - """Set color temperature in kelvin.""" - raise NotImplementedError( - "Called set_color_temperature on device that does not support it" - ) - - def set_rgb(self, rgb: Tuple[int, int, int], **kwargs): - """Set color in RGB.""" - raise NotImplementedError("Called set_rgb on device that does not support it") diff --git a/miio/interfaces/vacuuminterface.py b/miio/interfaces/vacuuminterface.py deleted file mode 100644 index d6ae7b892..000000000 --- a/miio/interfaces/vacuuminterface.py +++ /dev/null @@ -1,80 +0,0 @@ -"""`VacuumInterface` is an interface (abstract class) with shared API for all vacuum -devices.""" -from abc import abstractmethod -from enum import Enum, auto -from typing import Dict, Optional - -from miio import DeviceStatus - -# Dictionary of predefined fan speeds -FanspeedPresets = Dict[str, int] - - -class VacuumState(Enum): - """Vacuum state enum. - - This offers a simplified API to the vacuum state. - """ - - Unknown = auto() - Cleaning = auto() - Returning = auto() - Idle = auto() - Docked = auto() - Paused = auto() - Error = auto() - - -class VacuumDeviceStatus(DeviceStatus): - """Status container for vacuums.""" - - @abstractmethod - def vacuum_state(self) -> VacuumState: - """Return vacuum state.""" - - @abstractmethod - def error(self) -> Optional[str]: - """Return error message, if errored.""" - - @abstractmethod - def battery(self) -> Optional[int]: - """Return current battery charge, if available.""" - - -class VacuumInterface: - """Vacuum API interface.""" - - @abstractmethod - def home(self): - """Return vacuum robot to home station/dock.""" - - @abstractmethod - def start(self): - """Start cleaning.""" - - @abstractmethod - def stop(self): - """Stop cleaning.""" - - def pause(self): - """Pause cleaning. - - :raises RuntimeError: if the method is not supported by the device - """ - raise RuntimeError("`pause` not supported") - - @abstractmethod - def fan_speed_presets(self) -> FanspeedPresets: - """Return available fan speed presets. - - The returned object is a dictionary where the key is user-readable name and the - value is input for :func:`set_fan_speed_preset()`. - """ - - @abstractmethod - def set_fan_speed_preset(self, speed_preset: int) -> None: - """Set fan speed preset speed. - - :param speed_preset: a value from :func:`fan_speed_presets()` - :raises ValueError: for invalid preset value - """ diff --git a/miio/tests/test_vacuums.py b/miio/tests/test_vacuums.py deleted file mode 100644 index b2032f873..000000000 --- a/miio/tests/test_vacuums.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Test of vacuum devices.""" -from collections.abc import Iterable -from datetime import datetime -from typing import List, Sequence, Tuple, Type - -import pytest -from pytz import UTC - -from miio.device import Device -from miio.integrations.roborock.vacuum.vacuum import ROCKROBO_V1, Timer -from miio.interfaces import VacuumInterface - -# list of all supported vacuum classes -VACUUM_CLASSES: Tuple[Type[VacuumInterface], ...] = tuple( - cl for cl in VacuumInterface.__subclasses__() # type: ignore -) - - -def _all_vacuum_models() -> Sequence[Tuple[Type[Device], str]]: - """:return: list of tuples with supported vacuum models with corresponding class""" - result: List[Tuple[Type[Device], str]] = [] - for cls in VACUUM_CLASSES: - assert issubclass(cls, Device) - vacuum_models = cls.supported_models - assert isinstance(vacuum_models, Iterable) - for model in vacuum_models: - result.append((cls, model)) - return result # type: ignore - - -@pytest.mark.parametrize("cls, model", _all_vacuum_models()) -def test_vacuum_fan_speed_presets(cls: Type[Device], model: str) -> None: - """Test method VacuumInterface.fan_speed_presets()""" - if model == ROCKROBO_V1: - return # this model cannot be tested because presets depends on firmware - dev = cls("127.0.0.1", "68ffffffffffffffffffffffffffffff", model=model) - assert isinstance(dev, VacuumInterface) - presets = dev.fan_speed_presets() - assert presets is not None, "presets must be defined" - assert bool(presets), "presets cannot be empty" - assert isinstance(presets, dict), "presets must be dictionary" - for name, value in presets.items(): - assert isinstance(name, str), "presets key must be string" - assert name, "presets key cannot be empty" - assert isinstance(value, int), "presets value must be integer" - assert value >= 0, "presets value must be >= 0" - - -@pytest.mark.parametrize("cls, model", _all_vacuum_models()) -def test_vacuum_set_fan_speed_presets_fails(cls: Type[Device], model: str) -> None: - """Test method VacuumInterface.fan_speed_presets()""" - if model == ROCKROBO_V1: - return # this model cannot be tested because presets depends on firmware - dev = cls("127.0.0.1", "68ffffffffffffffffffffffffffffff", model=model) - assert isinstance(dev, VacuumInterface) - with pytest.raises(ValueError): - dev.set_fan_speed_preset(-1) - - -def test_vacuum_timer(mocker): - """Test Timer class.""" - - mock = mocker.patch.object(Timer, attribute="_now") - mock.return_value = datetime(2000, 1, 1) - - t = Timer( - data=["1488667794112", "off", ["49 22 * * 6", ["start_clean", ""]]], - timezone=UTC, - ) - - assert t.id == "1488667794112" - assert t.enabled is False - assert t.cron == "49 22 * * 6" - assert t.next_schedule == datetime( - 2000, 1, 1, 22, 49, tzinfo=UTC - ), "should figure out the next run" - assert t.next_schedule == datetime( - 2000, 1, 1, 22, 49, tzinfo=UTC - ), "should return the same value twice"