diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index c269ee53cf3..b825cace83a 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -18,13 +18,11 @@ from .const import CONF_DEVICE_KEY, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.COVER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenGarage from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - open_garage_connection = opengarage.OpenGarage( f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}", entry.data[CONF_DEVICE_KEY], @@ -36,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: open_garage_connection=open_garage_connection, ) await open_garage_data_coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = open_garage_data_coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = open_garage_data_coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/opengarage/button.py b/homeassistant/components/opengarage/button.py new file mode 100644 index 00000000000..9f676919098 --- /dev/null +++ b/homeassistant/components/opengarage/button.py @@ -0,0 +1,79 @@ +"""OpenGarage button.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, cast + +from opengarage import OpenGarage + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import OpenGarageDataUpdateCoordinator +from .const import DOMAIN +from .entity import OpenGarageEntity + + +@dataclass(frozen=True, kw_only=True) +class OpenGarageButtonEntityDescription(ButtonEntityDescription): + """OpenGarage Browser button description.""" + + press_action: Callable[[OpenGarage], Any] + + +BUTTONS: tuple[OpenGarageButtonEntityDescription, ...] = ( + OpenGarageButtonEntityDescription( + key="restart", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + press_action=lambda opengarage: opengarage.reboot(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the OpenGarage button entities.""" + coordinator: OpenGarageDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + OpenGarageButtonEntity( + coordinator, cast(str, config_entry.unique_id), description + ) + for description in BUTTONS + ) + + +class OpenGarageButtonEntity(OpenGarageEntity, ButtonEntity): + """Representation of an OpenGarage button.""" + + entity_description: OpenGarageButtonEntityDescription + + def __init__( + self, + coordinator: OpenGarageDataUpdateCoordinator, + device_id: str, + description: OpenGarageButtonEntityDescription, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator, device_id, description) + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.press_action( + self.coordinator.open_garage_connection + ) + await self.coordinator.async_refresh() diff --git a/tests/components/opengarage/conftest.py b/tests/components/opengarage/conftest.py new file mode 100644 index 00000000000..189c3a877ff --- /dev/null +++ b/tests/components/opengarage/conftest.py @@ -0,0 +1,59 @@ +"""Fixtures for the OpenGarage integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.opengarage.const import CONF_DEVICE_KEY, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Test device", + domain=DOMAIN, + data={ + CONF_HOST: "http://1.1.1.1", + CONF_PORT: "80", + CONF_DEVICE_KEY: "abc123", + CONF_VERIFY_SSL: False, + }, + unique_id="12345", + ) + + +@pytest.fixture +def mock_opengarage() -> Generator[MagicMock, None, None]: + """Return a mocked OpenGarage client.""" + with patch( + "homeassistant.components.opengarage.opengarage.OpenGarage", + autospec=True, + ) as client_mock: + client = client_mock.return_value + client.device_url = "http://1.1.1.1:80" + client.update_state.return_value = { + "name": "abcdef", + "mac": "aa:bb:cc:dd:ee:ff", + "fwv": "1.2.0", + } + yield client + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_opengarage: MagicMock +) -> MockConfigEntry: + """Set up the OpenGarage integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/opengarage/test_button.py b/tests/components/opengarage/test_button.py new file mode 100644 index 00000000000..b4557a116e8 --- /dev/null +++ b/tests/components/opengarage/test_button.py @@ -0,0 +1,33 @@ +"""Test the OpenGarage Browser buttons.""" +from unittest.mock import MagicMock + +import homeassistant.components.button as button +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_buttons( + hass: HomeAssistant, + mock_opengarage: MagicMock, + init_integration: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test standard OpenGarage buttons.""" + entry = entity_registry.async_get("button.abcdef_restart") + assert entry + assert entry.unique_id == "12345_restart" + await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.abcdef_restart"}, + blocking=True, + ) + assert len(mock_opengarage.reboot.mock_calls) == 1 + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry