diff --git a/homeassistant/components/niko_home_control/__init__.py b/homeassistant/components/niko_home_control/__init__.py index 37396e69caa..650d9f05b84 100644 --- a/homeassistant/components/niko_home_control/__init__.py +++ b/homeassistant/components/niko_home_control/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers import entity_registry as er from .const import _LOGGER -PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT] +PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT, Platform.SCENE] type NikoHomeControlConfigEntry = ConfigEntry[NHCController] diff --git a/homeassistant/components/niko_home_control/scene.py b/homeassistant/components/niko_home_control/scene.py new file mode 100644 index 00000000000..129b946b748 --- /dev/null +++ b/homeassistant/components/niko_home_control/scene.py @@ -0,0 +1,40 @@ +"""Scene Platform for Niko Home Control.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.scene import BaseScene +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import NikoHomeControlConfigEntry +from .entity import NikoHomeControlEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: NikoHomeControlConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Niko Home Control scene entry.""" + controller = entry.runtime_data + + async_add_entities( + NikoHomeControlScene(scene, controller, entry.entry_id) + for scene in controller.scenes + ) + + +class NikoHomeControlScene(NikoHomeControlEntity, BaseScene): + """Representation of a Niko Home Control Scene.""" + + _attr_name = None + + async def _async_activate(self, **kwargs: Any) -> None: + """Activate scene. Try to get entities into requested state.""" + await self._action.activate() + + def update_state(self) -> None: + """Update HA state.""" + self._async_record_activation() diff --git a/tests/components/niko_home_control/conftest.py b/tests/components/niko_home_control/conftest.py index b60a597a9ff..19890bf8d49 100644 --- a/tests/components/niko_home_control/conftest.py +++ b/tests/components/niko_home_control/conftest.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch from nhc.cover import NHCCover from nhc.light import NHCLight +from nhc.scene import NHCScene import pytest from homeassistant.components.niko_home_control.const import DOMAIN @@ -61,9 +62,21 @@ def cover() -> NHCCover: return mock +@pytest.fixture +def scene() -> NHCScene: + """Return a scene mock.""" + mock = AsyncMock(spec=NHCScene) + mock.id = 4 + mock.type = 0 + mock.name = "scene" + mock.suggested_area = "room" + mock.state = 0 + return mock + + @pytest.fixture def mock_niko_home_control_connection( - light: NHCLight, dimmable_light: NHCLight, cover: NHCCover + light: NHCLight, dimmable_light: NHCLight, cover: NHCCover, scene: NHCScene ) -> Generator[AsyncMock]: """Mock a NHC client.""" with ( @@ -79,6 +92,7 @@ def mock_niko_home_control_connection( client = mock_client.return_value client.lights = [light, dimmable_light] client.covers = [cover] + client.scenes = [scene] client.connect = AsyncMock(return_value=True) yield client diff --git a/tests/components/niko_home_control/snapshots/test_scene.ambr b/tests/components/niko_home_control/snapshots/test_scene.ambr new file mode 100644 index 00000000000..6887b1b373e --- /dev/null +++ b/tests/components/niko_home_control/snapshots/test_scene.ambr @@ -0,0 +1,49 @@ +# serializer version: 1 +# name: test_entities[scene.scene-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'scene', + 'entity_category': None, + 'entity_id': 'scene.scene', + '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, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[scene.scene-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'scene', + }), + 'context': , + 'entity_id': 'scene.scene', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-10-10T21:00:00+00:00', + }) +# --- diff --git a/tests/components/niko_home_control/test_scene.py b/tests/components/niko_home_control/test_scene.py new file mode 100644 index 00000000000..1f0868b1bbd --- /dev/null +++ b/tests/components/niko_home_control/test_scene.py @@ -0,0 +1,82 @@ +"""Tests for the Niko Home Control Scene platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, 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 + + +@pytest.mark.freeze_time("2025-10-10 21:00:00") +async def test_entities( + 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.SCENE] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("scene_id", [0]) +async def test_activate_scene( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + scene_id: int, + entity_registry: er.EntityRegistry, +) -> None: + """Test activating the scene.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SCENE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "scene.scene"}, + blocking=True, + ) + mock_niko_home_control_connection.scenes[scene_id].activate.assert_called_once() + + +async def test_updating( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + scene: AsyncMock, +) -> None: + """Test scene state recording after activation.""" + await setup_integration(hass, mock_config_entry) + + # Resolve the created scene entity dynamically + entity_entries = er.async_entries_for_config_entry( + er.async_get(hass), mock_config_entry.entry_id + ) + scene_entities = [e for e in entity_entries if e.domain == SCENE_DOMAIN] + assert scene_entities, "No scene entities registered" + entity_id = scene_entities[0].entity_id + + # Capture current state (could be unknown or a timestamp depending on implementation) + before = hass.states.get(entity_id) + assert before is not None + + # Simulate a device-originated update for the scene (controller callback) + await find_update_callback(mock_niko_home_control_connection, scene.id)(0) + await hass.async_block_till_done() + + after = hass.states.get(entity_id) + assert after is not None + assert after.state != before.state