Skip to content

Commit 8e49956

Browse files
wollewjoostlek
andauthored
Add reboot button to velux gateway device (home-assistant#155547)
Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent 5e0ebdd commit 8e49956

File tree

6 files changed

+216
-2
lines changed

6 files changed

+216
-2
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Support for VELUX KLF 200 gateway button."""
2+
3+
from __future__ import annotations
4+
5+
from pyvlx import PyVLX, PyVLXException
6+
7+
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
8+
from homeassistant.const import EntityCategory
9+
from homeassistant.core import HomeAssistant
10+
from homeassistant.exceptions import HomeAssistantError
11+
from homeassistant.helpers.device_registry import DeviceInfo
12+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
13+
14+
from . import VeluxConfigEntry
15+
from .const import DOMAIN
16+
17+
18+
async def async_setup_entry(
19+
hass: HomeAssistant,
20+
config_entry: VeluxConfigEntry,
21+
async_add_entities: AddConfigEntryEntitiesCallback,
22+
) -> None:
23+
"""Set up button entities for the Velux integration."""
24+
async_add_entities(
25+
[VeluxGatewayRebootButton(config_entry.entry_id, config_entry.runtime_data)]
26+
)
27+
28+
29+
class VeluxGatewayRebootButton(ButtonEntity):
30+
"""Representation of the Velux Gateway reboot button."""
31+
32+
_attr_has_entity_name = True
33+
_attr_device_class = ButtonDeviceClass.RESTART
34+
_attr_entity_category = EntityCategory.CONFIG
35+
36+
def __init__(self, config_entry_id: str, pyvlx: PyVLX) -> None:
37+
"""Initialize the gateway reboot button."""
38+
self.pyvlx = pyvlx
39+
self._attr_unique_id = f"{config_entry_id}_reboot-gateway"
40+
self._attr_device_info = DeviceInfo(
41+
identifiers={(DOMAIN, f"gateway_{config_entry_id}")},
42+
)
43+
44+
async def async_press(self) -> None:
45+
"""Handle the button press - reboot the gateway."""
46+
try:
47+
await self.pyvlx.reboot_gateway()
48+
except PyVLXException as ex:
49+
raise HomeAssistantError(
50+
translation_domain=DOMAIN,
51+
translation_key="reboot_failed",
52+
) from ex

homeassistant/components/velux/const.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,11 @@
55
from homeassistant.const import Platform
66

77
DOMAIN = "velux"
8-
PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.LIGHT, Platform.SCENE]
8+
PLATFORMS = [
9+
Platform.BINARY_SENSOR,
10+
Platform.BUTTON,
11+
Platform.COVER,
12+
Platform.LIGHT,
13+
Platform.SCENE,
14+
]
915
LOGGER = getLogger(__package__)

homeassistant/components/velux/strings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@
3636
}
3737
}
3838
},
39+
"exceptions": {
40+
"reboot_failed": {
41+
"message": "Failed to reboot gateway. Try again in a few moments or power cycle the device manually"
42+
}
43+
},
3944
"services": {
4045
"reboot_gateway": {
41-
"description": "Reboots the KLF200 Gateway.",
46+
"description": "Reboots the KLF200 Gateway",
4247
"name": "Reboot gateway"
4348
}
4449
}

tests/components/velux/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def mock_pyvlx(mock_window: MagicMock, mock_light: MagicMock) -> Generator[Magic
107107
def mock_config_entry() -> MockConfigEntry:
108108
"""Return a mock config entry."""
109109
return MockConfigEntry(
110+
entry_id="test_entry_id",
110111
domain=DOMAIN,
111112
data={
112113
CONF_HOST: "testhost",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# serializer version: 1
2+
# name: test_button_snapshot[button.klf_200_gateway_restart-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': 'button',
14+
'entity_category': <EntityCategory.CONFIG: 'config'>,
15+
'entity_id': 'button.klf_200_gateway_restart',
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': <ButtonDeviceClass.RESTART: 'restart'>,
26+
'original_icon': None,
27+
'original_name': 'Restart',
28+
'platform': 'velux',
29+
'previous_unique_id': None,
30+
'suggested_object_id': None,
31+
'supported_features': 0,
32+
'translation_key': None,
33+
'unique_id': 'test_entry_id_reboot-gateway',
34+
'unit_of_measurement': None,
35+
})
36+
# ---
37+
# name: test_button_snapshot[button.klf_200_gateway_restart-state]
38+
StateSnapshot({
39+
'attributes': ReadOnlyDict({
40+
'device_class': 'restart',
41+
'friendly_name': 'KLF 200 Gateway Restart',
42+
}),
43+
'context': <ANY>,
44+
'entity_id': 'button.klf_200_gateway_restart',
45+
'last_changed': <ANY>,
46+
'last_reported': <ANY>,
47+
'last_updated': <ANY>,
48+
'state': 'unknown',
49+
})
50+
# ---
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Test Velux button entities."""
2+
3+
from unittest.mock import AsyncMock, MagicMock
4+
5+
import pytest
6+
from pyvlx import PyVLXException
7+
from syrupy.assertion import SnapshotAssertion
8+
9+
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
10+
from homeassistant.components.velux import DOMAIN
11+
from homeassistant.const import ATTR_ENTITY_ID, Platform
12+
from homeassistant.core import HomeAssistant
13+
from homeassistant.exceptions import HomeAssistantError
14+
from homeassistant.helpers import device_registry as dr, entity_registry as er
15+
16+
from tests.common import MockConfigEntry, snapshot_platform
17+
18+
19+
@pytest.fixture
20+
def platform() -> Platform:
21+
"""Fixture to specify platform to test."""
22+
return Platform.BUTTON
23+
24+
25+
@pytest.mark.usefixtures("setup_integration")
26+
async def test_button_snapshot(
27+
hass: HomeAssistant,
28+
mock_config_entry: MockConfigEntry,
29+
entity_registry: er.EntityRegistry,
30+
device_registry: dr.DeviceRegistry,
31+
snapshot: SnapshotAssertion,
32+
) -> None:
33+
"""Snapshot the button entity (registry + state)."""
34+
await snapshot_platform(
35+
hass,
36+
entity_registry,
37+
snapshot,
38+
mock_config_entry.entry_id,
39+
)
40+
41+
# Get the button entity setup and test device association
42+
entity_entries = er.async_entries_for_config_entry(
43+
entity_registry, mock_config_entry.entry_id
44+
)
45+
assert len(entity_entries) == 1
46+
entry = entity_entries[0]
47+
48+
assert entry.device_id is not None
49+
device_entry = device_registry.async_get(entry.device_id)
50+
assert device_entry is not None
51+
assert (DOMAIN, f"gateway_{mock_config_entry.entry_id}") in device_entry.identifiers
52+
assert device_entry.via_device_id is None
53+
54+
55+
@pytest.mark.usefixtures("setup_integration")
56+
async def test_button_press_success(
57+
hass: HomeAssistant,
58+
mock_pyvlx: MagicMock,
59+
) -> None:
60+
"""Test successful button press."""
61+
62+
# Configure the mock method to be async and return a coroutine
63+
mock_pyvlx.reboot_gateway.return_value = AsyncMock()()
64+
65+
# Press the button
66+
await hass.services.async_call(
67+
BUTTON_DOMAIN,
68+
SERVICE_PRESS,
69+
{ATTR_ENTITY_ID: "button.klf_200_gateway_restart"},
70+
blocking=True,
71+
)
72+
73+
# Verify the reboot method was called
74+
mock_pyvlx.reboot_gateway.assert_called_once()
75+
76+
77+
@pytest.mark.usefixtures("setup_integration")
78+
async def test_button_press_failure(
79+
hass: HomeAssistant,
80+
mock_pyvlx: MagicMock,
81+
) -> None:
82+
"""Test button press failure handling."""
83+
84+
# Mock reboot failure
85+
mock_pyvlx.reboot_gateway.side_effect = PyVLXException("Connection failed")
86+
87+
# Press the button and expect HomeAssistantError
88+
with pytest.raises(
89+
HomeAssistantError,
90+
match="Failed to reboot gateway. Try again in a few moments or power cycle the device manually",
91+
):
92+
await hass.services.async_call(
93+
BUTTON_DOMAIN,
94+
SERVICE_PRESS,
95+
{ATTR_ENTITY_ID: "button.klf_200_gateway_restart"},
96+
blocking=True,
97+
)
98+
99+
# Verify the reboot method was called
100+
mock_pyvlx.reboot_gateway.assert_called_once()

0 commit comments

Comments
 (0)