diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index ece8a3b7f4a..f3db59a65ae 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -18,6 +18,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, + Platform.COVER, Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SELECT, diff --git a/homeassistant/components/tessie/cover.py b/homeassistant/components/tessie/cover.py new file mode 100644 index 00000000000..b7834a74766 --- /dev/null +++ b/homeassistant/components/tessie/cover.py @@ -0,0 +1,107 @@ +"""Cover platform for Tessie integration.""" +from __future__ import annotations + +from typing import Any + +from tessie_api import ( + close_charge_port, + close_windows, + open_unlock_charge_port, + vent_windows, +) + +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import TessieDataUpdateCoordinator +from .entity import TessieEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tessie sensor platform from a config entry.""" + coordinators = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + Entity(coordinator) + for Entity in ( + TessieWindowEntity, + TessieChargePortEntity, + ) + for coordinator in coordinators + ) + + +class TessieWindowEntity(TessieEntity, CoverEntity): + """Cover entity for current charge.""" + + _attr_device_class = CoverDeviceClass.WINDOW + _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + + def __init__(self, coordinator: TessieDataUpdateCoordinator) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, "windows") + + @property + 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 + ) + + 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), + ) + + 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), + ) + + +class TessieChargePortEntity(TessieEntity, CoverEntity): + """Cover entity for the charge port.""" + + _attr_device_class = CoverDeviceClass.DOOR + _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + + def __init__(self, coordinator: TessieDataUpdateCoordinator) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, "charge_state_charge_port_door_open") + + @property + def is_closed(self) -> bool | None: + """Return if the cover is closed or not.""" + return not self._value + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open windows.""" + await self.run(open_unlock_charge_port) + self.set((self.key, True)) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close windows.""" + await self.run(close_charge_port) + self.set((self.key, False)) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index a5de4e758e2..9bc6dfbd9bd 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -117,6 +117,14 @@ "name": "Passenger temperature setting" } }, + "cover": { + "windows": { + "name": "Vent windows" + }, + "charge_state_charge_port_door_open": { + "name": "Charge port door" + } + }, "select": { "climate_state_seat_heater_left": { "name": "Seat heater left", diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 12b001a83e6..c0f79d26a37 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -35,6 +35,11 @@ ERROR_TIMEOUT = ClientResponseError( ERROR_UNKNOWN = ClientResponseError( request_info=TEST_REQUEST_INFO, history=None, status=HTTPStatus.BAD_REQUEST ) +ERROR_VIRTUAL_KEY = ClientResponseError( + request_info=TEST_REQUEST_INFO, + history=None, + status=HTTPStatus.INTERNAL_SERVER_ERROR, +) ERROR_CONNECTION = ClientConnectionError() diff --git a/tests/components/tessie/test_cover.py b/tests/components/tessie/test_cover.py new file mode 100644 index 00000000000..be75b6df60a --- /dev/null +++ b/tests/components/tessie/test_cover.py @@ -0,0 +1,112 @@ +"""Test the Tessie cover platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.cover import ( + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + STATE_CLOSED, + STATE_OPEN, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .common import ERROR_UNKNOWN, TEST_RESPONSE, setup_platform + + +async def test_window(hass: HomeAssistant) -> 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 + + # Test open windows + with patch( + "homeassistant.components.tessie.cover.vent_windows", + 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 + + # Test close windows + with patch( + "homeassistant.components.tessie.cover.close_windows", + 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 + + +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.""" + + await setup_platform(hass) + entity_id = "cover.test_charge_port_door" + + # Test setting cover open with unknown error + with patch( + "homeassistant.components.tessie.cover.open_unlock_charge_port", + side_effect=ERROR_UNKNOWN, + ) as mock_set, pytest.raises(HomeAssistantError) as error: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_set.assert_called_once() + assert error.from_exception == ERROR_UNKNOWN