From 43a420cf0130b5126b11afe84365d04715085d32 Mon Sep 17 00:00:00 2001 From: "Glenn Vandeuren (aka Iondependent)" Date: Mon, 23 Dec 2024 15:47:09 +0100 Subject: [PATCH] Add cover to the niko_home_control integration (#133801) Co-authored-by: Joost Lekkerkerker --- .../components/niko_home_control/__init__.py | 2 +- .../components/niko_home_control/cover.py | 54 +++++++ .../components/niko_home_control/__init__.py | 15 ++ .../components/niko_home_control/conftest.py | 16 +- .../snapshots/test_cover.ambr | 48 ++++++ .../niko_home_control/test_cover.py | 138 ++++++++++++++++++ .../niko_home_control/test_light.py | 10 +- 7 files changed, 275 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/niko_home_control/cover.py create mode 100644 tests/components/niko_home_control/snapshots/test_cover.ambr create mode 100644 tests/components/niko_home_control/test_cover.py diff --git a/homeassistant/components/niko_home_control/__init__.py b/homeassistant/components/niko_home_control/__init__.py index 0bc1b117a70..ae4e8986816 100644 --- a/homeassistant/components/niko_home_control/__init__.py +++ b/homeassistant/components/niko_home_control/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers import entity_registry as er from .const import _LOGGER -PLATFORMS: list[Platform] = [Platform.LIGHT] +PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT] type NikoHomeControlConfigEntry = ConfigEntry[NHCController] diff --git a/homeassistant/components/niko_home_control/cover.py b/homeassistant/components/niko_home_control/cover.py new file mode 100644 index 00000000000..51e2a8a702d --- /dev/null +++ b/homeassistant/components/niko_home_control/cover.py @@ -0,0 +1,54 @@ +"""Cover Platform for Niko Home Control.""" + +from __future__ import annotations + +from typing import Any + +from nhc.cover import NHCCover + +from homeassistant.components.cover import CoverEntity, CoverEntityFeature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import NikoHomeControlConfigEntry +from .entity import NikoHomeControlEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: NikoHomeControlConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Niko Home Control cover entry.""" + controller = entry.runtime_data + + async_add_entities( + NikoHomeControlCover(cover, controller, entry.entry_id) + for cover in controller.covers + ) + + +class NikoHomeControlCover(NikoHomeControlEntity, CoverEntity): + """Representation of a Niko Cover.""" + + _attr_name = None + _attr_supported_features: CoverEntityFeature = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + ) + _action: NHCCover + + def open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + self._action.open() + + def close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + self._action.close() + + def stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + self._action.stop() + + def update_state(self): + """Update HA state.""" + self._attr_is_closed = self._action.state == 0 diff --git a/tests/components/niko_home_control/__init__.py b/tests/components/niko_home_control/__init__.py index f6e8187bf0f..0182a24ba7c 100644 --- a/tests/components/niko_home_control/__init__.py +++ b/tests/components/niko_home_control/__init__.py @@ -1,5 +1,10 @@ """Tests for the niko_home_control integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import AsyncMock + +import pytest + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -11,3 +16,13 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + + +def find_update_callback( + mock: AsyncMock, identifier: int +) -> Callable[[int], Awaitable[None]]: + """Find the update callback for a specific identifier.""" + for call in mock.register_callback.call_args_list: + if call[0][0] == identifier: + return call[0][1] + pytest.fail(f"Callback for identifier {identifier} not found") diff --git a/tests/components/niko_home_control/conftest.py b/tests/components/niko_home_control/conftest.py index b3dedd0c182..130baf72228 100644 --- a/tests/components/niko_home_control/conftest.py +++ b/tests/components/niko_home_control/conftest.py @@ -3,6 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch +from nhc.cover import NHCCover from nhc.light import NHCLight import pytest @@ -48,9 +49,21 @@ def dimmable_light() -> NHCLight: return mock +@pytest.fixture +def cover() -> NHCCover: + """Return a cover mock.""" + mock = AsyncMock(spec=NHCCover) + mock.id = 3 + mock.type = 4 + mock.name = "cover" + mock.suggested_area = "room" + mock.state = 100 + return mock + + @pytest.fixture def mock_niko_home_control_connection( - light: NHCLight, dimmable_light: NHCLight + light: NHCLight, dimmable_light: NHCLight, cover: NHCCover ) -> Generator[AsyncMock]: """Mock a NHC client.""" with ( @@ -65,6 +78,7 @@ def mock_niko_home_control_connection( ): client = mock_client.return_value client.lights = [light, dimmable_light] + client.covers = [cover] yield client diff --git a/tests/components/niko_home_control/snapshots/test_cover.ambr b/tests/components/niko_home_control/snapshots/test_cover.ambr new file mode 100644 index 00000000000..6f99c1adb8c --- /dev/null +++ b/tests/components/niko_home_control/snapshots/test_cover.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_cover[cover.cover-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.cover', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'niko_home_control', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover[cover.cover-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'cover', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.cover', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/niko_home_control/test_cover.py b/tests/components/niko_home_control/test_cover.py new file mode 100644 index 00000000000..5e9a17c3324 --- /dev/null +++ b/tests/components/niko_home_control/test_cover.py @@ -0,0 +1,138 @@ +"""Tests for the Niko Home Control cover platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_OPEN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import find_update_callback, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_cover( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.niko_home_control.PLATFORMS", [Platform.COVER] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("cover_id", "entity_id"), + [ + (0, "cover.cover"), + ], +) +async def test_open_cover( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + cover_id: int, + entity_id: int, +) -> None: + """Test opening the cover.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_niko_home_control_connection.covers[cover_id].open.assert_called_once_with() + + +@pytest.mark.parametrize( + ("cover_id", "entity_id"), + [ + (0, "cover.cover"), + ], +) +async def test_close_cover( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + cover_id: int, + entity_id: str, +) -> None: + """Test closing the cover.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_niko_home_control_connection.covers[cover_id].close.assert_called_once_with() + + +@pytest.mark.parametrize( + ("cover_id", "entity_id"), + [ + (0, "cover.cover"), + ], +) +async def test_stop_cover( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + cover_id: int, + entity_id: str, +) -> None: + """Test closing the cover.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_niko_home_control_connection.covers[cover_id].stop.assert_called_once_with() + + +async def test_updating( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + cover: AsyncMock, +) -> None: + """Test closing the cover.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("cover.cover").state == STATE_OPEN + + cover.state = 0 + await find_update_callback(mock_niko_home_control_connection, 3)(0) + await hass.async_block_till_done() + + assert hass.states.get("cover.cover").state == STATE_CLOSED + + cover.state = 100 + await find_update_callback(mock_niko_home_control_connection, 3)(100) + await hass.async_block_till_done() + + assert hass.states.get("cover.cover").state == STATE_OPEN diff --git a/tests/components/niko_home_control/test_light.py b/tests/components/niko_home_control/test_light.py index 801bdf6a296..a61cc5204f6 100644 --- a/tests/components/niko_home_control/test_light.py +++ b/tests/components/niko_home_control/test_light.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration +from . import find_update_callback, setup_integration from tests.common import MockConfigEntry, snapshot_platform @@ -113,7 +113,7 @@ async def test_updating( assert hass.states.get("light.light").state == STATE_ON light.state = 0 - await mock_niko_home_control_connection.register_callback.call_args_list[0][0][1](0) + await find_update_callback(mock_niko_home_control_connection, 1)(0) await hass.async_block_till_done() assert hass.states.get("light.light").state == STATE_OFF @@ -122,16 +122,14 @@ async def test_updating( assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 255 dimmable_light.state = 80 - await mock_niko_home_control_connection.register_callback.call_args_list[1][0][1]( - 80 - ) + await find_update_callback(mock_niko_home_control_connection, 2)(80) await hass.async_block_till_done() assert hass.states.get("light.dimmable_light").state == STATE_ON assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 204 dimmable_light.state = 0 - await mock_niko_home_control_connection.register_callback.call_args_list[1][0][1](0) + await find_update_callback(mock_niko_home_control_connection, 2)(0) await hass.async_block_till_done() assert hass.states.get("light.dimmable_light").state == STATE_OFF