Move Trunks from Button to Cover in Tessie (#106448)

* Move trunks from buttons to covers

* Add tests

* Cleanup snapshot

* Use Constants

* StrEnum to IntEnum
This commit is contained in:
Brett Adams 2023-12-27 15:56:09 +10:00 committed by GitHub
parent 2fe982c7f3
commit 91aea843fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 181 additions and 92 deletions

View File

@ -9,8 +9,6 @@ from tessie_api import (
enable_keyless_driving,
flash_lights,
honk,
open_close_rear_trunk,
open_front_trunk,
trigger_homelink,
wake,
)
@ -49,12 +47,6 @@ DESCRIPTIONS: tuple[TessieButtonEntityDescription, ...] = (
TessieButtonEntityDescription(
key="boombox", func=lambda: boombox, icon="mdi:volume-high"
),
TessieButtonEntityDescription(
key="frunk", func=lambda: open_front_trunk, icon="mdi:car"
),
TessieButtonEntityDescription(
key="trunk", func=lambda: open_close_rear_trunk, icon="mdi:car-back"
),
)

View File

@ -1,7 +1,7 @@
"""Constants used by Tessie integration."""
from __future__ import annotations
from enum import StrEnum
from enum import IntEnum, StrEnum
DOMAIN = "tessie"
@ -46,3 +46,10 @@ class TessieUpdateStatus(StrEnum):
INSTALLING = "installing"
WIFI_WAIT = "downloading_wifi_wait"
SCHEDULED = "scheduled"
class TessieCoverStates(IntEnum):
"""Tessie Cover states."""
CLOSED = 0
OPEN = 1

View File

@ -6,6 +6,8 @@ from typing import Any
from tessie_api import (
close_charge_port,
close_windows,
open_close_rear_trunk,
open_front_trunk,
open_unlock_charge_port,
vent_windows,
)
@ -19,7 +21,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .const import DOMAIN, TessieCoverStates
from .coordinator import TessieStateUpdateCoordinator
from .entity import TessieEntity
@ -31,10 +33,12 @@ async def async_setup_entry(
data = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
Entity(vehicle.state_coordinator)
for Entity in (
klass(vehicle.state_coordinator)
for klass in (
TessieWindowEntity,
TessieChargePortEntity,
TessieFrontTrunkEntity,
TessieRearTrunkEntity,
)
for vehicle in data
)
@ -54,30 +58,30 @@ class TessieWindowEntity(TessieEntity, CoverEntity):
def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
return (
self.get("vehicle_state_fd_window") == 0
and self.get("vehicle_state_fp_window") == 0
and self.get("vehicle_state_rd_window") == 0
and self.get("vehicle_state_rp_window") == 0
self.get("vehicle_state_fd_window") == TessieCoverStates.CLOSED
and self.get("vehicle_state_fp_window") == TessieCoverStates.CLOSED
and self.get("vehicle_state_rd_window") == TessieCoverStates.CLOSED
and self.get("vehicle_state_rp_window") == TessieCoverStates.CLOSED
)
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open windows."""
await self.run(vent_windows)
self.set(
("vehicle_state_fd_window", 1),
("vehicle_state_fp_window", 1),
("vehicle_state_rd_window", 1),
("vehicle_state_rp_window", 1),
("vehicle_state_fd_window", TessieCoverStates.OPEN),
("vehicle_state_fp_window", TessieCoverStates.OPEN),
("vehicle_state_rd_window", TessieCoverStates.OPEN),
("vehicle_state_rp_window", TessieCoverStates.OPEN),
)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close windows."""
await self.run(close_windows)
self.set(
("vehicle_state_fd_window", 0),
("vehicle_state_fp_window", 0),
("vehicle_state_rd_window", 0),
("vehicle_state_rp_window", 0),
("vehicle_state_fd_window", TessieCoverStates.CLOSED),
("vehicle_state_fp_window", TessieCoverStates.CLOSED),
("vehicle_state_rd_window", TessieCoverStates.CLOSED),
("vehicle_state_rp_window", TessieCoverStates.CLOSED),
)
@ -105,3 +109,52 @@ class TessieChargePortEntity(TessieEntity, CoverEntity):
"""Close windows."""
await self.run(close_charge_port)
self.set((self.key, False))
class TessieFrontTrunkEntity(TessieEntity, CoverEntity):
"""Cover entity for the charge port."""
_attr_device_class = CoverDeviceClass.DOOR
_attr_supported_features = CoverEntityFeature.OPEN
def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, "vehicle_state_ft")
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
return self._value == TessieCoverStates.CLOSED
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open front trunk."""
await self.run(open_front_trunk)
self.set((self.key, TessieCoverStates.OPEN))
class TessieRearTrunkEntity(TessieEntity, CoverEntity):
"""Cover entity for the charge port."""
_attr_device_class = CoverDeviceClass.DOOR
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
def __init__(self, coordinator: TessieStateUpdateCoordinator) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, "vehicle_state_rt")
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
return self._value == TessieCoverStates.CLOSED
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open rear trunk."""
if self._value == TessieCoverStates.CLOSED:
await self.run(open_close_rear_trunk)
self.set((self.key, TessieCoverStates.OPEN))
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close rear trunk."""
if self._value == TessieCoverStates.OPEN:
await self.run(open_close_rear_trunk)
self.set((self.key, TessieCoverStates.CLOSED))

View File

@ -123,7 +123,9 @@
},
"charge_state_charge_port_door_open": {
"name": "Charge port door"
}
},
"vehicle_state_ft": { "name": "Frunk" },
"vehicle_state_rt": { "name": "Trunk" }
},
"select": {
"climate_state_seat_heater_left": {
@ -267,9 +269,7 @@
"honk": { "name": "Honk horn" },
"trigger_homelink": { "name": "Homelink" },
"enable_keyless_driving": { "name": "Keyless driving" },
"boombox": { "name": "Play fart" },
"frunk": { "name": "Open frunk" },
"trunk": { "name": "Open/Close trunk" }
"boombox": { "name": "Play fart" }
},
"switch": {
"charge_state_charge_enable_request": {

View File

@ -0,0 +1,57 @@
# serializer version: 1
# name: test_covers[cover.test_charge_port_door-open_unlock_charge_port-close_charge_port][cover.test_charge_port_door]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'Test Charge port door',
'supported_features': <CoverEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'cover.test_charge_port_door',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'open',
})
# ---
# name: test_covers[cover.test_frunk-open_front_trunk-False][cover.test_frunk]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'Test Frunk',
'supported_features': <CoverEntityFeature: 1>,
}),
'context': <ANY>,
'entity_id': 'cover.test_frunk',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---
# name: test_covers[cover.test_trunk-open_close_rear_trunk-open_close_rear_trunk][cover.test_trunk]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'door',
'friendly_name': 'Test Trunk',
'supported_features': <CoverEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'cover.test_trunk',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---
# name: test_covers[cover.test_vent_windows-vent_windows-close_windows][cover.test_vent_windows]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'window',
'friendly_name': 'Test Vent windows',
'supported_features': <CoverEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'cover.test_vent_windows',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---

View File

@ -2,6 +2,7 @@
from unittest.mock import patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
@ -17,80 +18,59 @@ from homeassistant.exceptions import HomeAssistantError
from .common import ERROR_UNKNOWN, TEST_RESPONSE, TEST_RESPONSE_ERROR, setup_platform
async def test_window(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
("entity_id", "openfunc", "closefunc"),
[
("cover.test_vent_windows", "vent_windows", "close_windows"),
("cover.test_charge_port_door", "open_unlock_charge_port", "close_charge_port"),
("cover.test_frunk", "open_front_trunk", False),
("cover.test_trunk", "open_close_rear_trunk", "open_close_rear_trunk"),
],
)
async def test_covers(
hass: HomeAssistant,
entity_id: str,
openfunc: str,
closefunc: str,
snapshot: SnapshotAssertion,
) -> None:
"""Tests that the window cover entity is correct."""
await setup_platform(hass)
entity_id = "cover.test_vent_windows"
assert hass.states.get(entity_id).state == STATE_CLOSED
assert hass.states.get(entity_id) == snapshot(name=entity_id)
# Test open windows
if openfunc:
with patch(
"homeassistant.components.tessie.cover.vent_windows",
f"homeassistant.components.tessie.cover.{openfunc}",
return_value=TEST_RESPONSE,
) as mock_set:
) as mock_open:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
mock_set.assert_called_once()
mock_open.assert_called_once()
assert hass.states.get(entity_id).state == STATE_OPEN
# Test close windows
if closefunc:
with patch(
"homeassistant.components.tessie.cover.close_windows",
f"homeassistant.components.tessie.cover.{closefunc}",
return_value=TEST_RESPONSE,
) as mock_set:
) as mock_close:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
mock_set.assert_called_once()
mock_close.assert_called_once()
assert hass.states.get(entity_id).state == STATE_CLOSED
async def test_charge_port(hass: HomeAssistant) -> None:
"""Tests that the charge port cover entity is correct."""
await setup_platform(hass)
entity_id = "cover.test_charge_port_door"
assert hass.states.get(entity_id).state == STATE_OPEN
# Test close charge port
with patch(
"homeassistant.components.tessie.cover.close_charge_port",
return_value=TEST_RESPONSE,
) as mock_set:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
mock_set.assert_called_once()
assert hass.states.get(entity_id).state == STATE_CLOSED
# Test open charge port
with patch(
"homeassistant.components.tessie.cover.open_unlock_charge_port",
return_value=TEST_RESPONSE,
) as mock_set:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
mock_set.assert_called_once()
assert hass.states.get(entity_id).state == STATE_OPEN
async def test_errors(hass: HomeAssistant) -> None:
"""Tests errors are handled."""