Skip to content

Commit

Permalink
feat: add scheduled charging (#360)
Browse files Browse the repository at this point in the history
closes #370
  • Loading branch information
InTheDaylight14 authored Dec 6, 2022
1 parent c419a04 commit f68b928
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 2 deletions.
158 changes: 156 additions & 2 deletions teslajsonpy/car.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
7: "third_row_right",
}

DAY_SELECTION_MAP = {
"all_week": False,
"weekdays": True,
}


class TeslaCar:
# pylint: disable=too-many-public-methods
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
78 changes: 78 additions & 0 deletions tests/unit_tests/test_car.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
VIN,
)

DAY_SELECTION_MAP = {
"all_week": False,
"weekdays": True,
}


@pytest.mark.asyncio
async def test_car_properties(monkeypatch):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

0 comments on commit f68b928

Please sign in to comment.