Add cover to the niko_home_control integration (#133801)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Glenn Vandeuren (aka Iondependent) 2024-12-23 15:47:09 +01:00 committed by GitHub
parent 70648da8fd
commit 43a420cf01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 275 additions and 8 deletions

View File

@ -13,7 +13,7 @@ from homeassistant.helpers import entity_registry as er
from .const import _LOGGER from .const import _LOGGER
PLATFORMS: list[Platform] = [Platform.LIGHT] PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT]
type NikoHomeControlConfigEntry = ConfigEntry[NHCController] type NikoHomeControlConfigEntry = ConfigEntry[NHCController]

View File

@ -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

View File

@ -1,5 +1,10 @@
"""Tests for the niko_home_control integration.""" """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 homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry 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.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() 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")

View File

@ -3,6 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from nhc.cover import NHCCover
from nhc.light import NHCLight from nhc.light import NHCLight
import pytest import pytest
@ -48,9 +49,21 @@ def dimmable_light() -> NHCLight:
return mock 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 @pytest.fixture
def mock_niko_home_control_connection( def mock_niko_home_control_connection(
light: NHCLight, dimmable_light: NHCLight light: NHCLight, dimmable_light: NHCLight, cover: NHCCover
) -> Generator[AsyncMock]: ) -> Generator[AsyncMock]:
"""Mock a NHC client.""" """Mock a NHC client."""
with ( with (
@ -65,6 +78,7 @@ def mock_niko_home_control_connection(
): ):
client = mock_client.return_value client = mock_client.return_value
client.lights = [light, dimmable_light] client.lights = [light, dimmable_light]
client.covers = [cover]
yield client yield client

View File

@ -0,0 +1,48 @@
# serializer version: 1
# name: test_cover[cover.cover-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.cover',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <CoverEntityFeature: 11>,
'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': <CoverEntityFeature: 11>,
}),
'context': <ANY>,
'entity_id': 'cover.cover',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'open',
})
# ---

View File

@ -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

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er 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 from tests.common import MockConfigEntry, snapshot_platform
@ -113,7 +113,7 @@ async def test_updating(
assert hass.states.get("light.light").state == STATE_ON assert hass.states.get("light.light").state == STATE_ON
light.state = 0 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() await hass.async_block_till_done()
assert hass.states.get("light.light").state == STATE_OFF 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 assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 255
dimmable_light.state = 80 dimmable_light.state = 80
await mock_niko_home_control_connection.register_callback.call_args_list[1][0][1]( await find_update_callback(mock_niko_home_control_connection, 2)(80)
80
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("light.dimmable_light").state == STATE_ON assert hass.states.get("light.dimmable_light").state == STATE_ON
assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 204 assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 204
dimmable_light.state = 0 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() await hass.async_block_till_done()
assert hass.states.get("light.dimmable_light").state == STATE_OFF assert hass.states.get("light.dimmable_light").state == STATE_OFF