From 6cbf1da76a9ed0359a0f762f4627a39d6335efb7 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 14 Jan 2024 04:03:10 +1000 Subject: [PATCH] Add charge cable lock to Tessie (#107212) * Add cable lock * Translate exception * Use ServiceValidationError --- homeassistant/components/tessie/const.py | 7 ++++ homeassistant/components/tessie/lock.py | 42 ++++++++++++++++++-- homeassistant/components/tessie/strings.json | 8 ++++ tests/components/tessie/test_lock.py | 27 ++++++++++++- 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index 2ba4e514579..28981b87e6d 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -53,3 +53,10 @@ class TessieCoverStates(IntEnum): CLOSED = 0 OPEN = 1 + + +class TessieChargeCableLockStates(StrEnum): + """Tessie Charge Cable Lock states.""" + + ENGAGED = "Engaged" + DISENGAGED = "Disengaged" diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index e8fb8930bbc..1a0d879cd79 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -3,14 +3,15 @@ from __future__ import annotations from typing import Any -from tessie_api import lock, unlock +from tessie_api import lock, open_unlock_charge_port, unlock from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, TessieChargeCableLockStates from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity @@ -21,11 +22,15 @@ async def async_setup_entry( """Set up the Tessie sensor platform from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] - async_add_entities(TessieLockEntity(vehicle.state_coordinator) for vehicle in data) + async_add_entities( + klass(vehicle.state_coordinator) + for klass in (TessieLockEntity, TessieCableLockEntity) + for vehicle in data + ) class TessieLockEntity(TessieEntity, LockEntity): - """Lock entity for current charge.""" + """Lock entity for Tessie.""" def __init__( self, @@ -48,3 +53,32 @@ class TessieLockEntity(TessieEntity, LockEntity): """Set new value.""" await self.run(unlock) self.set((self.key, False)) + + +class TessieCableLockEntity(TessieEntity, LockEntity): + """Cable Lock entity for Tessie.""" + + def __init__( + self, + coordinator: TessieStateUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, "charge_state_charge_port_latch") + + @property + def is_locked(self) -> bool | None: + """Return the state of the Lock.""" + return self._value == TessieChargeCableLockStates.ENGAGED + + async def async_lock(self, **kwargs: Any) -> None: + """Charge cable Lock cannot be manually locked.""" + raise ServiceValidationError( + "Insert cable to lock", + translation_domain=DOMAIN, + translation_key="no_cable", + ) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock charge cable lock.""" + await self.run(open_unlock_charge_port) + self.set((self.key, TessieChargeCableLockStates.DISENGAGED)) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 7cf511c125c..c2483b1be8c 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -56,6 +56,9 @@ "lock": { "vehicle_state_locked": { "name": "[%key:component::lock::title%]" + }, + "charge_state_charge_port_latch": { + "name": "Charge cable lock" } }, "media_player": { @@ -315,5 +318,10 @@ "name": "[%key:component::update::title%]" } } + }, + "exceptions": { + "no_cable": { + "message": "Insert cable to lock" + } } } diff --git a/tests/components/tessie/test_lock.py b/tests/components/tessie/test_lock.py index 93a1151a850..d1cbdfe1fa9 100644 --- a/tests/components/tessie/test_lock.py +++ b/tests/components/tessie/test_lock.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, @@ -9,6 +11,7 @@ from homeassistant.components.lock import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from .common import TEST_VEHICLE_STATE_ONLINE, setup_platform @@ -20,7 +23,7 @@ async def test_locks(hass: HomeAssistant) -> None: await setup_platform(hass) - assert len(hass.states.async_all("lock")) == 1 + assert len(hass.states.async_all("lock")) == 2 entity_id = "lock.test_lock" @@ -48,3 +51,25 @@ async def test_locks(hass: HomeAssistant) -> None: ) assert hass.states.get(entity_id).state == STATE_UNLOCKED mock_run.assert_called_once() + + # Test charge cable lock set value functions + entity_id = "lock.test_charge_cable_lock" + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + + with patch( + "homeassistant.components.tessie.lock.open_unlock_charge_port" + ) as mock_run: + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_UNLOCKED + mock_run.assert_called_once()