Skip to content

Commit

Permalink
feat: add trunk and frunk management (#71)
Browse files Browse the repository at this point in the history
* add trunk sensor

* add frunk sensor

* add frunk sensor

* add frunk sensor

* add trunk switch

* add frunk switch

* add trunk lock

* add frunk lock

* fix lint R0915 too-many-statements

* rename open_trunk to open, same for close

* fix some attributes to work with HA integration

* 0.7.0

Automatically generated by python-semantic-release

* remove unecessary sensors and switches

* add last update handling

Co-authored-by: Alan Tse <alandtse@users.noreply.github.com>
Co-authored-by: semantic-release <semantic-release>
  • Loading branch information
hobbe and alandtse committed Apr 16, 2020
1 parent ca180de commit 142e44b
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 15 deletions.
3 changes: 3 additions & 0 deletions teslajsonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from teslajsonpy.gps import GPS, Odometer
from teslajsonpy.lock import Lock
from teslajsonpy.sentry_mode import SentryModeSwitch
from teslajsonpy.trunk import TrunkLock, FrunkLock

from .__version__ import __version__

Expand All @@ -39,5 +40,7 @@
"Odometer",
"Lock",
"SentryModeSwitch",
"TrunkLock",
"FrunkLock",
"__version__",
]
1 change: 1 addition & 0 deletions teslajsonpy/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def __init__(self, data: Dict, controller) -> None:
self.__online_state: bool = None
self.type: Text = "online sensor"
self.hass_type = "binary_sensor"
self.sensor_type = "connectivity"
self.name: Text = self._name()
self.uniq_name: Text = self._uniq_name()

Expand Down
36 changes: 21 additions & 15 deletions teslajsonpy/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from teslajsonpy.gps import GPS, Odometer
from teslajsonpy.lock import ChargerLock, Lock
from teslajsonpy.sentry_mode import SentryModeSwitch
from teslajsonpy.trunk import TrunkLock, FrunkLock

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -287,21 +288,7 @@ async def connect(
self.__driving[vin] = {}
self.__gui[vin] = {}

self.__components.append(Climate(car, self))
self.__components.append(Battery(car, self))
self.__components.append(Range(car, self))
self.__components.append(TempSensor(car, self))
self.__components.append(Lock(car, self))
self.__components.append(ChargerLock(car, self))
self.__components.append(ChargerConnectionSensor(car, self))
self.__components.append(ChargingSensor(car, self))
self.__components.append(ChargerSwitch(car, self))
self.__components.append(RangeSwitch(car, self))
self.__components.append(ParkingSensor(car, self))
self.__components.append(GPS(car, self))
self.__components.append(Odometer(car, self))
self.__components.append(OnlineSensor(car, self))
self.__components.append(SentryModeSwitch(car, self))
self._add_components(car)

if not test_login:
tasks = [
Expand Down Expand Up @@ -489,6 +476,25 @@ def get_homeassistant_components(self):
"""
return self.__components

def _add_components(self, car):
self.__components.append(Climate(car, self))
self.__components.append(Battery(car, self))
self.__components.append(Range(car, self))
self.__components.append(TempSensor(car, self))
self.__components.append(Lock(car, self))
self.__components.append(ChargerLock(car, self))
self.__components.append(ChargerConnectionSensor(car, self))
self.__components.append(ChargingSensor(car, self))
self.__components.append(ChargerSwitch(car, self))
self.__components.append(RangeSwitch(car, self))
self.__components.append(ParkingSensor(car, self))
self.__components.append(GPS(car, self))
self.__components.append(Odometer(car, self))
self.__components.append(OnlineSensor(car, self))
self.__components.append(SentryModeSwitch(car, self))
self.__components.append(TrunkLock(car, self))
self.__components.append(FrunkLock(car, self))

async def _wake_up(self, car_id):
car_vin = self._id_to_vin(car_id)
car_id = self._update_id(car_id)
Expand Down
132 changes: 132 additions & 0 deletions teslajsonpy/trunk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# SPDX-License-Identifier: Apache-2.0
"""
Python Package for controlling Tesla API.
For more details about this api, please refer to the documentation at
https://github.com/zabuldon/teslajsonpy
"""
import time

from typing import Text

from teslajsonpy.vehicle import VehicleDevice


class TrunkLock(VehicleDevice):
"""Home-Assistant rear trunk lock for a Tesla VehicleDevice."""

def __init__(self, data, controller):
"""Initialize the rear trunk lock.
Args:
data (Dict): The vehicle state for a Tesla vehicle.
https://tesla-api.timdorr.com/vehicle/state/vehiclestate
controller (Controller): The controller that controls updates to the Tesla API.
"""
super().__init__(data, controller)
self.__lock_state: int = None
self.type: Text = "trunk lock"
self.hass_type: Text = "lock"
self.sensor_type: Text = "door"
self.bin_type = 0x7
self.name: Text = self._name()
self.uniq_name: Text = self._uniq_name()
self.__manual_update_time = 0

async def async_update(self, wake_if_asleep=False, force=False) -> None:
"""Update the rear trunk state."""
await super().async_update(wake_if_asleep=wake_if_asleep, force=force)
last_update = self._controller.get_last_update_time(self._id)
if last_update >= self.__manual_update_time:
data = self._controller.get_state_params(self._id)
self.__lock_state = data["rt"] if (data and "rt" in data) else None

def is_locked(self):
"""Return whether the rear trunk is closed."""
return self.__lock_state == 0

async def unlock(self):
"""Open the rear trunk."""
if self.is_locked():
data = await self._controller.command(
self._id, "actuate_trunk", {"which_trunk": "rear"}, wake_if_asleep=True
)
if data and data["response"]["result"]:
self.__lock_state = 255
self.__manual_update_time = time.time()

async def lock(self):
"""Close the rear trunk."""
if not self.is_locked():
data = await self._controller.command(
self._id, "actuate_trunk", {"which_trunk": "rear"}, wake_if_asleep=True
)
if data and data["response"]["result"]:
self.__lock_state = 0
self.__manual_update_time = time.time()

@staticmethod
def has_battery():
"""Return whether the device has a battery."""
return False


class FrunkLock(VehicleDevice):
"""Home-Assistant front trunk (frunk) lock for a Tesla VehicleDevice."""

def __init__(self, data, controller):
"""Initialize the front trunk (frunk) lock.
Args:
data (Dict): The vehicle state for a Tesla vehicle.
https://tesla-api.timdorr.com/vehicle/state/vehiclestate
controller (Controller): The controller that controls updates to the Tesla API.
"""
super().__init__(data, controller)
self.__lock_state: int = None
self.type: Text = "frunk lock"
self.hass_type: Text = "lock"
self.sensor_type: Text = "door"
self.bin_type = 0x7
self.name: Text = self._name()
self.uniq_name: Text = self._uniq_name()
self.__manual_update_time = 0

async def async_update(self, wake_if_asleep=False, force=False) -> None:
"""Update the front trunk (frunk) state."""
await super().async_update(wake_if_asleep=wake_if_asleep, force=force)
last_update = self._controller.get_last_update_time(self._id)
if last_update >= self.__manual_update_time:
data = self._controller.get_state_params(self._id)
self.__lock_state = data["ft"] if (data and "ft" in data) else None

def is_locked(self):
"""Return whether the front trunk (frunk) is closed."""
return self.__lock_state == 0

async def unlock(self):
"""Open the front trunk (frunk)."""
if self.is_locked():
data = await self._controller.command(
self._id, "actuate_trunk", {"which_trunk": "front"}, wake_if_asleep=True
)
if data and data["response"]["result"]:
self.__lock_state = 255
self.__manual_update_time = time.time()

async def lock(self):
"""Close the front trunk (frunk)."""
if not self.is_locked():
data = await self._controller.command(
self._id, "actuate_trunk", {"which_trunk": "front"}, wake_if_asleep=True
)
if data and data["response"]["result"]:
self.__lock_state = 0
self.__manual_update_time = time.time()

@staticmethod
def has_battery():
"""Return whether the device has a battery."""
return False
128 changes: 128 additions & 0 deletions tests/unit_tests/test_frunk_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Test frunk lock."""

import pytest

from tests.tesla_mock import TeslaMock

from teslajsonpy.controller import Controller
from teslajsonpy.trunk import FrunkLock


def test_has_battery(monkeypatch):
"""Test has_battery()."""

_mock = TeslaMock(monkeypatch)
_controller = Controller(None)

_data = _mock.data_request_vehicle()
_lock = FrunkLock(_data, _controller)

assert not _lock.has_battery()


def test_is_locked_on_init(monkeypatch):
"""Test is_locked() after initialization."""

_mock = TeslaMock(monkeypatch)
_controller = Controller(None)

_data = _mock.data_request_vehicle()
_lock = FrunkLock(_data, _controller)

assert _lock is not None
assert not _lock.is_locked()


@pytest.mark.asyncio
async def test_is_locked_after_update(monkeypatch):
"""Test is_locked() after an update."""

_mock = TeslaMock(monkeypatch)
_controller = Controller(None)

_data = _mock.data_request_vehicle()
_data["vehicle_state"]["ft"] = 0
_lock = FrunkLock(_data, _controller)

await _lock.async_update()

assert _lock is not None
assert _lock.is_locked()


@pytest.mark.asyncio
async def test_unlock(monkeypatch):
"""Test unlock()."""

_mock = TeslaMock(monkeypatch)
_controller = Controller(None)

_data = _mock.data_request_vehicle()
_data["vehicle_state"]["ft"] = 0
_lock = FrunkLock(_data, _controller)

await _lock.async_update()
await _lock.unlock()

assert _lock is not None
assert not _lock.is_locked()


@pytest.mark.asyncio
async def test_unlock_already_unlocked(monkeypatch):
"""Test unlock() when already unlocked."""

_mock = TeslaMock(monkeypatch)
_controller = Controller(None)

_data = _mock.data_request_vehicle()
_data["vehicle_state"]["ft"] = 123
_lock = FrunkLock(_data, _controller)

await _lock.async_update()
await _lock.unlock()

assert _lock is not None
assert not _lock.is_locked()

# Reset to default for next tests
_data["vehicle_state"]["ft"] = 0


@pytest.mark.asyncio
async def test_lock(monkeypatch):
"""Test lock()."""

_mock = TeslaMock(monkeypatch)
_controller = Controller(None)

_data = _mock.data_request_vehicle()
_data["vehicle_state"]["ft"] = 123
_lock = FrunkLock(_data, _controller)

await _lock.async_update()
await _lock.lock()

assert _lock is not None
assert _lock.is_locked()

# Reset to default for next tests
_data["vehicle_state"]["ft"] = 0


@pytest.mark.asyncio
async def test_lock_already_locked(monkeypatch):
"""Test lock() when already locked."""

_mock = TeslaMock(monkeypatch)
_controller = Controller(None)

_data = _mock.data_request_vehicle()
_data["vehicle_state"]["ft"] = 0
_lock = FrunkLock(_data, _controller)

await _lock.async_update()
await _lock.lock()

assert _lock is not None
assert _lock.is_locked()
Loading

0 comments on commit 142e44b

Please sign in to comment.