From 142e44b18cebe3fda04d5d64a6aa48cfb3bacb37 Mon Sep 17 00:00:00 2001 From: Olivier B Date: Thu, 16 Apr 2020 21:44:17 +0200 Subject: [PATCH] feat: add trunk and frunk management (#71) * 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 Co-authored-by: semantic-release --- teslajsonpy/__init__.py | 3 + teslajsonpy/binary_sensor.py | 1 + teslajsonpy/controller.py | 36 ++++---- teslajsonpy/trunk.py | 132 ++++++++++++++++++++++++++++ tests/unit_tests/test_frunk_lock.py | 128 +++++++++++++++++++++++++++ tests/unit_tests/test_trunk_lock.py | 128 +++++++++++++++++++++++++++ 6 files changed, 413 insertions(+), 15 deletions(-) create mode 100644 teslajsonpy/trunk.py create mode 100644 tests/unit_tests/test_frunk_lock.py create mode 100644 tests/unit_tests/test_trunk_lock.py diff --git a/teslajsonpy/__init__.py b/teslajsonpy/__init__.py index b6371cf7..6f8f0196 100644 --- a/teslajsonpy/__init__.py +++ b/teslajsonpy/__init__.py @@ -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__ @@ -39,5 +40,7 @@ "Odometer", "Lock", "SentryModeSwitch", + "TrunkLock", + "FrunkLock", "__version__", ] diff --git a/teslajsonpy/binary_sensor.py b/teslajsonpy/binary_sensor.py index af24cdc4..b34ca2ab 100644 --- a/teslajsonpy/binary_sensor.py +++ b/teslajsonpy/binary_sensor.py @@ -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() diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 3d677582..52656f48 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -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__) @@ -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 = [ @@ -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) diff --git a/teslajsonpy/trunk.py b/teslajsonpy/trunk.py new file mode 100644 index 00000000..cec112a6 --- /dev/null +++ b/teslajsonpy/trunk.py @@ -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 diff --git a/tests/unit_tests/test_frunk_lock.py b/tests/unit_tests/test_frunk_lock.py new file mode 100644 index 00000000..a73a6442 --- /dev/null +++ b/tests/unit_tests/test_frunk_lock.py @@ -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() diff --git a/tests/unit_tests/test_trunk_lock.py b/tests/unit_tests/test_trunk_lock.py new file mode 100644 index 00000000..f7026fc2 --- /dev/null +++ b/tests/unit_tests/test_trunk_lock.py @@ -0,0 +1,128 @@ +"""Test trunk lock.""" + +import pytest + +from tests.tesla_mock import TeslaMock + +from teslajsonpy.controller import Controller +from teslajsonpy.trunk import TrunkLock + + +def test_has_battery(monkeypatch): + """Test has_battery().""" + + _mock = TeslaMock(monkeypatch) + _controller = Controller(None) + + _data = _mock.data_request_vehicle() + _lock = TrunkLock(_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 = TrunkLock(_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"]["rt"] = 0 + _lock = TrunkLock(_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"]["rt"] = 0 + _lock = TrunkLock(_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"]["rt"] = 123 + _lock = TrunkLock(_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"]["rt"] = 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"]["rt"] = 123 + _lock = TrunkLock(_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"]["rt"] = 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"]["rt"] = 0 + _lock = TrunkLock(_data, _controller) + + await _lock.async_update() + await _lock.lock() + + assert _lock is not None + assert _lock.is_locked()