diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index f0c20df6..eec69b8b 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -29,6 +29,11 @@ 7: "third_row_right", } +DAY_SELECTION_MAP = { + "all_week": False, + "weekdays": True, +} + class TeslaCar: # pylint: disable=too-many-public-methods @@ -570,16 +575,80 @@ def is_valet_mode(self) -> bool: @property def is_auto_seat_climate_left(self) -> bool: - """Return state of valet mode.""" + """Return state of auto seat climate left.""" return self._vehicle_data.get("climate_state", {}).get("auto_seat_climate_left") @property def is_auto_seat_climate_right(self) -> bool: - """Return state of valet mode.""" + """Return state of auto seat climate right.""" return self._vehicle_data.get("climate_state", {}).get( "auto_seat_climate_right" ) + @property + def scheduled_departure_time(self) -> int: + """Return the scheduled departure time.""" + return self._vehicle_data.get("charge_state", {}).get( + "scheduled_departure_time" + ) + + @property + def scheduled_departure_time_minutes(self) -> int: + """Return the scheduled departure time in minutes after midnight.""" + return self._vehicle_data.get("charge_state", {}).get( + "scheduled_departure_time_minutes" + ) + + @property + def is_off_peak_charging_enabled(self) -> bool: + """Return if peak charging is enabled for scheduled departure.""" + return self._vehicle_data.get("charge_state", {}).get( + "off_peak_charging_enabled" + ) + + @property + def is_off_peak_charging_weekday_only(self) -> bool: + """Return if off off peak charging is weekday only for scheduled departure.""" + return DAY_SELECTION_MAP.get( + self._vehicle_data.get("charge_state", {}).get("off_peak_charging_times") + ) + + @property + def off_peak_hours_end_time(self) -> int: + """Return end of off peak hours in minutes after midnight for scheduled departure.""" + return self._vehicle_data.get("charge_state", {}).get("off_peak_hours_end_time") + + @property + def is_preconditioning_enabled(self) -> bool: + """Return if preconditioning is enabled for scheduled departure.""" + return self._vehicle_data.get("charge_state", {}).get("preconditioning_enabled") + + @property + def is_preconditioning_weekday_only(self) -> bool: + """Return if preconditioning is weekday only for scheduled departure.""" + return DAY_SELECTION_MAP.get( + self._vehicle_data.get("charge_state", {}).get("preconditioning_times") + ) + + @property + def scheduled_charging_mode(self) -> str: + """Return 'Off', 'DepartBy', or 'StartAt' for schedule disabled, scheduled departure, and scheduled charging respectively.""" + return self._vehicle_data.get("charge_state", {}).get("scheduled_charging_mode") + + @property + def is_scheduled_charging_pending(self) -> bool: + """Return if scheduled charging is pending.""" + return self._vehicle_data.get("charge_state", {}).get( + "scheduled_charging_pending" + ) + + @property + def scheduled_charging_start_time_app(self) -> int: + """Return the scheduled charging start time.""" + return self._vehicle_data.get("charge_state", {}).get( + "scheduled_charging_start_time_app" + ) + async def _send_command( self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs ) -> dict: @@ -1121,3 +1190,88 @@ async def remote_start(self) -> None: _LOGGER.debug("Error calling remote start: %s", reason) else: self._vehicle_data["vehicle_state"].update({"remote_start": True}) + + async def set_scheduled_departure( + self, + enable: bool, + departure_time: int, + preconditioning_enabled: bool, + preconditioning_weekdays_only: bool, + off_peak_charging_enabled: bool, + off_peak_charging_weekdays_only: bool, + end_off_peak_time: int, + ) -> None: + """Send command to set depature time. + + Args + enable: Turn on (True) or turn off (False) the scheduled departure. + departure_time: Time in minutes after midnight (local time) for the departure. + preconditioning_enabled: Enable (True) or disbale (False) the climate preconditioning. + preconditioning_weekdays_only: Precondition climate for departure time on weekdays only (True) or all days (False). + off_peak_charging_enabled: Complete charging durring off peak hours (True) or complete charging just before departure time (False). + off_peak_charging_weekdays_only: Complete off peak charging only on weekdays only (True) or all days (False). + end_off_peak_time: Time in minutes after midnight when the off peak rate ends. + + """ + data = await self._send_command( + "SCHEDULED_DEPARTURE", + path_vars={"vehicle_id": self.id}, + enable=enable, + departure_time=departure_time, + preconditioning_enabled=preconditioning_enabled, + preconditioning_weekdays_only=preconditioning_weekdays_only, + off_peak_charging_enabled=off_peak_charging_enabled, + off_peak_charging_weekdays_only=off_peak_charging_weekdays_only, + end_off_peak_time=end_off_peak_time, + wake_if_asleep=True, + ) + + if data and data["response"]["result"] is True: + if enable: + mode_str = "DepartBy" + else: + mode_str = "Off" + + params = { + "scheduled_charging_mode": mode_str, + "scheduled_departure_time_minutes": departure_time, + "preconditioning_enabled": preconditioning_enabled, + "preconditioning_weekdays_only": list(DAY_SELECTION_MAP.values()).index( + preconditioning_weekdays_only + ), + "off_peak_charging_enabled": off_peak_charging_enabled, + "off_peak_charging_weekdays_only": list( + DAY_SELECTION_MAP.values() + ).index(off_peak_charging_weekdays_only), + "end_off_peak_time": end_off_peak_time, + } + self._vehicle_data["charge_state"].update(params) + + async def set_scheduled_charging(self, enable: bool, time: int) -> None: + """Send command to set scheduled charging time. + + Args + enable: Turn on (True) or turn off (False) the scheduled charging. + time: Time in minutes after midnight (local time) to start charging. + + """ + data = await self._send_command( + "SCHEDULED_CHARGING", + path_vars={"vehicle_id": self.id}, + enable=enable, + time=time, + wake_if_asleep=True, + ) + + if data and data["response"]["result"] is True: + if enable: + mode_str = "StartAt" + else: + mode_str = "Off" + time = None + params = { + "scheduled_charging_mode": mode_str, + "scheduled_charging_start_time": time, + "scheduled_charging_pending": enable, + } + self._vehicle_data["charge_state"].update(params) diff --git a/tests/unit_tests/test_car.py b/tests/unit_tests/test_car.py index 9fe29071..645cbb40 100644 --- a/tests/unit_tests/test_car.py +++ b/tests/unit_tests/test_car.py @@ -10,6 +10,11 @@ VIN, ) +DAY_SELECTION_MAP = { + "all_week": False, + "weekdays": True, +} + @pytest.mark.asyncio async def test_car_properties(monkeypatch): @@ -263,6 +268,45 @@ async def test_car_properties(monkeypatch): == VEHICLE_DATA["climate_state"]["auto_seat_climate_right"] ) + assert ( + _car.scheduled_departure_time + == VEHICLE_DATA["charge_state"]["scheduled_departure_time"] + ) + + assert ( + _car.scheduled_departure_time_minutes + == VEHICLE_DATA["charge_state"]["scheduled_departure_time_minutes"] + ) + + assert _car.is_off_peak_charging_enabled + + assert _car.is_off_peak_charging_weekday_only == DAY_SELECTION_MAP.get( + VEHICLE_DATA["charge_state"]["off_peak_charging_times"] + ) + + assert ( + _car.off_peak_hours_end_time + == VEHICLE_DATA["charge_state"]["off_peak_hours_end_time"] + ) + + assert _car.is_preconditioning_enabled is False + + assert _car.is_preconditioning_weekday_only == DAY_SELECTION_MAP.get( + VEHICLE_DATA["charge_state"]["preconditioning_times"] + ) + + assert ( + _car.scheduled_charging_mode + == VEHICLE_DATA["charge_state"]["scheduled_charging_mode"] + ) + + assert _car.is_scheduled_charging_pending is False + + assert ( + _car.scheduled_charging_start_time_app + == VEHICLE_DATA["charge_state"]["scheduled_charging_start_time_app"] + ) + @pytest.mark.asyncio async def test_change_charge_limit(monkeypatch): @@ -593,3 +637,37 @@ async def test_remote_start(monkeypatch): _car = _controller.cars[VIN] assert await _car.remote_start() is None + + +@pytest.mark.asyncio +async def test_set_scheduled_departure(monkeypatch): + """Test set scheduled departure.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + await _controller.generate_car_objects() + _car = _controller.cars[VIN] + + assert ( + await _car.set_scheduled_departure(True, 420, True, False, False, False, 480) + is None + ) + + assert ( + await _car.set_scheduled_departure(False, 460, False, True, True, True, 500) + is None + ) + + +@pytest.mark.asyncio +async def test_set_scheduled_charging(monkeypatch): + """Test set scheduled charging.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + await _controller.generate_car_objects() + _car = _controller.cars[VIN] + + assert await _car.set_scheduled_charging(True, 420) is None + + assert await _car.set_scheduled_charging(False, 420) is None