Skip to content

Commit a48b915

Browse files
VandeurenGlennCopilotjoostlek
authored
Add scene platform support to Niko Home Control integration (#152712)
Co-authored-by: Copilot <[email protected]> Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent e8227ba commit a48b915

File tree

5 files changed

+187
-2
lines changed

5 files changed

+187
-2
lines changed

homeassistant/components/niko_home_control/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from .const import _LOGGER
1414

15-
PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT]
15+
PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT, Platform.SCENE]
1616

1717
type NikoHomeControlConfigEntry = ConfigEntry[NHCController]
1818

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Scene Platform for Niko Home Control."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from homeassistant.components.scene import BaseScene
8+
from homeassistant.core import HomeAssistant
9+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
10+
11+
from . import NikoHomeControlConfigEntry
12+
from .entity import NikoHomeControlEntity
13+
14+
15+
async def async_setup_entry(
16+
hass: HomeAssistant,
17+
entry: NikoHomeControlConfigEntry,
18+
async_add_entities: AddConfigEntryEntitiesCallback,
19+
) -> None:
20+
"""Set up the Niko Home Control scene entry."""
21+
controller = entry.runtime_data
22+
23+
async_add_entities(
24+
NikoHomeControlScene(scene, controller, entry.entry_id)
25+
for scene in controller.scenes
26+
)
27+
28+
29+
class NikoHomeControlScene(NikoHomeControlEntity, BaseScene):
30+
"""Representation of a Niko Home Control Scene."""
31+
32+
_attr_name = None
33+
34+
async def _async_activate(self, **kwargs: Any) -> None:
35+
"""Activate scene. Try to get entities into requested state."""
36+
await self._action.activate()
37+
38+
def update_state(self) -> None:
39+
"""Update HA state."""
40+
self._async_record_activation()

tests/components/niko_home_control/conftest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from nhc.cover import NHCCover
77
from nhc.light import NHCLight
8+
from nhc.scene import NHCScene
89
import pytest
910

1011
from homeassistant.components.niko_home_control.const import DOMAIN
@@ -61,9 +62,21 @@ def cover() -> NHCCover:
6162
return mock
6263

6364

65+
@pytest.fixture
66+
def scene() -> NHCScene:
67+
"""Return a scene mock."""
68+
mock = AsyncMock(spec=NHCScene)
69+
mock.id = 4
70+
mock.type = 0
71+
mock.name = "scene"
72+
mock.suggested_area = "room"
73+
mock.state = 0
74+
return mock
75+
76+
6477
@pytest.fixture
6578
def mock_niko_home_control_connection(
66-
light: NHCLight, dimmable_light: NHCLight, cover: NHCCover
79+
light: NHCLight, dimmable_light: NHCLight, cover: NHCCover, scene: NHCScene
6780
) -> Generator[AsyncMock]:
6881
"""Mock a NHC client."""
6982
with (
@@ -79,6 +92,7 @@ def mock_niko_home_control_connection(
7992
client = mock_client.return_value
8093
client.lights = [light, dimmable_light]
8194
client.covers = [cover]
95+
client.scenes = [scene]
8296
client.connect = AsyncMock(return_value=True)
8397
yield client
8498

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# serializer version: 1
2+
# name: test_entities[scene.scene-entry]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': None,
8+
'config_entry_id': <ANY>,
9+
'config_subentry_id': <ANY>,
10+
'device_class': None,
11+
'device_id': <ANY>,
12+
'disabled_by': None,
13+
'domain': 'scene',
14+
'entity_category': None,
15+
'entity_id': 'scene.scene',
16+
'has_entity_name': True,
17+
'hidden_by': None,
18+
'icon': None,
19+
'id': <ANY>,
20+
'labels': set({
21+
}),
22+
'name': None,
23+
'options': dict({
24+
}),
25+
'original_device_class': None,
26+
'original_icon': None,
27+
'original_name': None,
28+
'platform': 'niko_home_control',
29+
'previous_unique_id': None,
30+
'suggested_object_id': None,
31+
'supported_features': 0,
32+
'translation_key': None,
33+
'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-4',
34+
'unit_of_measurement': None,
35+
})
36+
# ---
37+
# name: test_entities[scene.scene-state]
38+
StateSnapshot({
39+
'attributes': ReadOnlyDict({
40+
'friendly_name': 'scene',
41+
}),
42+
'context': <ANY>,
43+
'entity_id': 'scene.scene',
44+
'last_changed': <ANY>,
45+
'last_reported': <ANY>,
46+
'last_updated': <ANY>,
47+
'state': '2025-10-10T21:00:00+00:00',
48+
})
49+
# ---
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Tests for the Niko Home Control Scene platform."""
2+
3+
from unittest.mock import AsyncMock, patch
4+
5+
import pytest
6+
from syrupy.assertion import SnapshotAssertion
7+
8+
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN
9+
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, Platform
10+
from homeassistant.core import HomeAssistant
11+
from homeassistant.helpers import entity_registry as er
12+
13+
from . import find_update_callback, setup_integration
14+
15+
from tests.common import MockConfigEntry, snapshot_platform
16+
17+
18+
@pytest.mark.freeze_time("2025-10-10 21:00:00")
19+
async def test_entities(
20+
hass: HomeAssistant,
21+
snapshot: SnapshotAssertion,
22+
mock_niko_home_control_connection: AsyncMock,
23+
mock_config_entry: MockConfigEntry,
24+
entity_registry: er.EntityRegistry,
25+
) -> None:
26+
"""Test all entities."""
27+
with patch(
28+
"homeassistant.components.niko_home_control.PLATFORMS", [Platform.SCENE]
29+
):
30+
await setup_integration(hass, mock_config_entry)
31+
32+
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
33+
34+
35+
@pytest.mark.parametrize("scene_id", [0])
36+
async def test_activate_scene(
37+
hass: HomeAssistant,
38+
mock_niko_home_control_connection: AsyncMock,
39+
mock_config_entry: MockConfigEntry,
40+
scene_id: int,
41+
entity_registry: er.EntityRegistry,
42+
) -> None:
43+
"""Test activating the scene."""
44+
await setup_integration(hass, mock_config_entry)
45+
46+
await hass.services.async_call(
47+
SCENE_DOMAIN,
48+
SERVICE_TURN_ON,
49+
{ATTR_ENTITY_ID: "scene.scene"},
50+
blocking=True,
51+
)
52+
mock_niko_home_control_connection.scenes[scene_id].activate.assert_called_once()
53+
54+
55+
async def test_updating(
56+
hass: HomeAssistant,
57+
mock_niko_home_control_connection: AsyncMock,
58+
mock_config_entry: MockConfigEntry,
59+
scene: AsyncMock,
60+
) -> None:
61+
"""Test scene state recording after activation."""
62+
await setup_integration(hass, mock_config_entry)
63+
64+
# Resolve the created scene entity dynamically
65+
entity_entries = er.async_entries_for_config_entry(
66+
er.async_get(hass), mock_config_entry.entry_id
67+
)
68+
scene_entities = [e for e in entity_entries if e.domain == SCENE_DOMAIN]
69+
assert scene_entities, "No scene entities registered"
70+
entity_id = scene_entities[0].entity_id
71+
72+
# Capture current state (could be unknown or a timestamp depending on implementation)
73+
before = hass.states.get(entity_id)
74+
assert before is not None
75+
76+
# Simulate a device-originated update for the scene (controller callback)
77+
await find_update_callback(mock_niko_home_control_connection, scene.id)(0)
78+
await hass.async_block_till_done()
79+
80+
after = hass.states.get(entity_id)
81+
assert after is not None
82+
assert after.state != before.state

0 commit comments

Comments
 (0)