From 8918a9c2c4cddd956fec534449fbf5cc26e19890 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 22 Dec 2023 12:40:55 +1000 Subject: [PATCH] Add button platform to Tessie (#106210) --- homeassistant/components/tessie/__init__.py | 1 + homeassistant/components/tessie/button.py | 84 ++++++++++++++++++++ homeassistant/components/tessie/strings.json | 8 ++ tests/components/tessie/common.py | 17 +++- tests/components/tessie/test_button.py | 24 ++++++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tessie/button.py create mode 100644 tests/components/tessie/test_button.py diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index b360541ef1e..608be7692dc 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -16,6 +16,7 @@ from .coordinator import TessieDataUpdateCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.DEVICE_TRACKER, Platform.SELECT, diff --git a/homeassistant/components/tessie/button.py b/homeassistant/components/tessie/button.py new file mode 100644 index 00000000000..fb4449f5898 --- /dev/null +++ b/homeassistant/components/tessie/button.py @@ -0,0 +1,84 @@ +"""Button platform for Tessie integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from tessie_api import ( + boombox, + enable_keyless_driving, + flash_lights, + honk, + open_close_rear_trunk, + open_front_trunk, + trigger_homelink, + wake, +) + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +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 + + +@dataclass(frozen=True, kw_only=True) +class TessieButtonEntityDescription(ButtonEntityDescription): + """Describes a Tessie Button entity.""" + + func: Callable + + +DESCRIPTIONS: tuple[TessieButtonEntityDescription, ...] = ( + TessieButtonEntityDescription(key="wake", func=wake, icon="mdi:sleep-off"), + TessieButtonEntityDescription( + key="flash_lights", func=flash_lights, icon="mdi:flashlight" + ), + TessieButtonEntityDescription(key="honk", func=honk, icon="mdi:bullhorn"), + TessieButtonEntityDescription( + key="trigger_homelink", func=trigger_homelink, icon="mdi:garage" + ), + TessieButtonEntityDescription( + key="enable_keyless_driving", func=enable_keyless_driving, icon="mdi:car-key" + ), + TessieButtonEntityDescription(key="boombox", func=boombox, icon="mdi:volume-high"), + TessieButtonEntityDescription(key="frunk", func=open_front_trunk, icon="mdi:car"), + TessieButtonEntityDescription( + key="trunk", func=open_close_rear_trunk, icon="mdi:car-back" + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tessie Button platform from a config entry.""" + coordinators = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + TessieButtonEntity(coordinator, description) + for coordinator in coordinators + for description in DESCRIPTIONS + ) + + +class TessieButtonEntity(TessieEntity, ButtonEntity): + """Base class for Tessie Buttons.""" + + entity_description: TessieButtonEntityDescription + + def __init__( + self, + coordinator: TessieDataUpdateCoordinator, + description: TessieButtonEntityDescription, + ) -> None: + """Initialize the Button.""" + super().__init__(coordinator, description.key) + self.entity_description = description + + async def async_press(self) -> None: + """Press the button.""" + await self.run(self.entity_description.func) diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index a583a6d66eb..a5de4e758e2 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -241,6 +241,14 @@ "name": "Tire pressure warning rear right" } }, + "button": { + "wake": { "name": "Wake" }, + "flash_lights": { "name": "Flash lights" }, + "honk": { "name": "Honk horn" }, + "trigger_homelink": { "name": "Homelink" }, + "enable_keyless_driving": { "name": "Keyless driving" }, + "boombox": { "name": "Play fart" } + }, "switch": { "charge_state_charge_enable_request": { "name": "Charge" diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 30b6feca4d7..12b001a83e6 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -1,7 +1,8 @@ """Tessie common helpers for tests.""" +from contextlib import contextmanager from http import HTTPStatus -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aiohttp import ClientConnectionError, ClientResponseError from aiohttp.client import RequestInfo @@ -9,6 +10,7 @@ from aiohttp.client import RequestInfo from homeassistant.components.tessie.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription from tests.common import MockConfigEntry, load_json_object_fixture @@ -54,3 +56,16 @@ async def setup_platform(hass: HomeAssistant, side_effect=None): await hass.async_block_till_done() return mock_entry + + +@contextmanager +def patch_description( + key: str, attr: str, descriptions: tuple[EntityDescription] +) -> AsyncMock: + """Patch a description.""" + to_patch = next(filter(lambda x: x.key == key, descriptions)) + original = to_patch.func + mock = AsyncMock() + object.__setattr__(to_patch, attr, mock) + yield mock + object.__setattr__(to_patch, attr, original) diff --git a/tests/components/tessie/test_button.py b/tests/components/tessie/test_button.py new file mode 100644 index 00000000000..cd98377cb0c --- /dev/null +++ b/tests/components/tessie/test_button.py @@ -0,0 +1,24 @@ +"""Test the Tessie button platform.""" + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.tessie.button import DESCRIPTIONS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .common import patch_description, setup_platform + + +async def test_buttons(hass: HomeAssistant) -> None: + """Tests that the buttons are correct.""" + + await setup_platform(hass) + + # Test wake button + with patch_description("wake", "func", DESCRIPTIONS) as mock_wake: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ["button.test_wake"]}, + blocking=True, + ) + mock_wake.assert_called_once()