mirror of
https://github.com/home-assistant/core.git
synced 2026-02-14 23:28:42 +00:00
243 lines
8.1 KiB
Python
243 lines
8.1 KiB
Python
"""Test Velux light entities."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS,
|
|
DOMAIN as LIGHT_DOMAIN,
|
|
SERVICE_TURN_OFF,
|
|
SERVICE_TURN_ON,
|
|
)
|
|
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
|
|
from . import update_callback_entity
|
|
|
|
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
|
|
|
|
# Apply setup_integration fixture to all tests in this module
|
|
pytestmark = pytest.mark.usefixtures("setup_integration")
|
|
|
|
|
|
@pytest.fixture
|
|
def platform() -> Platform:
|
|
"""Fixture to specify platform to test."""
|
|
return Platform.LIGHT
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mock_pyvlx", ["mock_light", "mock_onoff_light"], indirect=True
|
|
)
|
|
async def test_light_setup(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
entity_registry: er.EntityRegistry,
|
|
snapshot: SnapshotAssertion,
|
|
) -> None:
|
|
"""Snapshot the entity and validate registry metadata for light entities."""
|
|
await snapshot_platform(
|
|
hass,
|
|
entity_registry,
|
|
snapshot,
|
|
mock_config_entry.entry_id,
|
|
)
|
|
|
|
|
|
async def test_light_device_association(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
device_registry: dr.DeviceRegistry,
|
|
mock_light: AsyncMock,
|
|
) -> None:
|
|
"""Test light device association."""
|
|
|
|
test_entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
|
|
|
|
# Get entity + device entry
|
|
entity_entry = entity_registry.async_get(test_entity_id)
|
|
assert entity_entry is not None
|
|
assert entity_entry.device_id is not None
|
|
device_entry = device_registry.async_get(entity_entry.device_id)
|
|
assert device_entry is not None
|
|
|
|
# Verify device has correct identifiers + name
|
|
assert ("velux", mock_light.serial_number) in device_entry.identifiers
|
|
assert device_entry.name == mock_light.name
|
|
|
|
|
|
# This test is not light specific, it just uses the light platform to test the base entity class.
|
|
async def test_entity_callbacks(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_light: AsyncMock,
|
|
) -> None:
|
|
"""Ensure the entity unregisters its device-updated callback when unloaded."""
|
|
# Entity is created by setup_integration; callback should be registered
|
|
test_entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
|
|
state = hass.states.get(test_entity_id)
|
|
assert state is not None
|
|
|
|
# Callback is registered exactly once with a callable
|
|
assert mock_light.register_device_updated_cb.call_count == 1
|
|
cb = mock_light.register_device_updated_cb.call_args[0][0]
|
|
assert callable(cb)
|
|
|
|
# Unload the config entry to trigger async_will_remove_from_hass
|
|
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# Callback must be unregistered with the same callable
|
|
assert mock_light.unregister_device_updated_cb.call_count == 1
|
|
assert mock_light.unregister_device_updated_cb.call_args[0][0] is cb
|
|
|
|
|
|
# Test availability functionality by using the light platform
|
|
async def test_entity_availability(
|
|
hass: HomeAssistant, mock_light: AsyncMock, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test that entity availability updates based on device connection status."""
|
|
|
|
entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
|
|
|
|
# Initially connected
|
|
mock_light.pyvlx.get_connected.return_value = True
|
|
await update_callback_entity(hass, mock_light)
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert state.state != STATE_UNAVAILABLE
|
|
|
|
# Simulate disconnection
|
|
mock_light.pyvlx.get_connected.return_value = False
|
|
await update_callback_entity(hass, mock_light)
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert state.state == STATE_UNAVAILABLE
|
|
assert caplog.text.count(f"Entity {entity_id} is unavailable") == 1
|
|
|
|
# Simulate disconnection, check we don't log again
|
|
mock_light.pyvlx.get_connected.return_value = False
|
|
await update_callback_entity(hass, mock_light)
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert state.state == STATE_UNAVAILABLE
|
|
assert caplog.text.count(f"Entity {entity_id} is unavailable") == 1
|
|
|
|
# Simulate reconnection
|
|
mock_light.pyvlx.get_connected.return_value = True
|
|
await update_callback_entity(hass, mock_light)
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert state.state != STATE_UNAVAILABLE
|
|
assert caplog.text.count(f"Entity {entity_id} is back online") == 1
|
|
|
|
# Simulate reconnection, check we don't log again
|
|
mock_light.pyvlx.get_connected.return_value = True
|
|
await update_callback_entity(hass, mock_light)
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert state.state != STATE_UNAVAILABLE
|
|
assert caplog.text.count(f"Entity {entity_id} is back online") == 1
|
|
|
|
|
|
async def test_light_brightness_and_is_on(
|
|
hass: HomeAssistant, mock_light: AsyncMock
|
|
) -> None:
|
|
"""Validate brightness mapping and on/off state from intensity."""
|
|
|
|
entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
|
|
|
|
# Set initial intensity values
|
|
mock_light.intensity.intensity_percent = 20 # 20% "intensity" -> 20% brightness
|
|
mock_light.intensity.off = False
|
|
mock_light.intensity.known = True
|
|
|
|
# Trigger state write
|
|
await update_callback_entity(hass, mock_light)
|
|
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
# brightness = int(20 * 255 / 100) = int(51)
|
|
assert state.attributes.get(ATTR_BRIGHTNESS) == 51
|
|
assert state.state == STATE_ON
|
|
|
|
# Mark as off
|
|
mock_light.intensity.off = True
|
|
await update_callback_entity(hass, mock_light)
|
|
state = hass.states.get(entity_id)
|
|
assert state is not None
|
|
assert state.state == STATE_OFF
|
|
|
|
|
|
async def test_light_turn_on_with_brightness_uses_set_intensity(
|
|
hass: HomeAssistant, mock_light: AsyncMock
|
|
) -> None:
|
|
"""Turning on with brightness calls set_intensity with percent."""
|
|
|
|
entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
|
|
|
|
# Call turn_on with brightness=51 (20% when normalized)
|
|
await hass.services.async_call(
|
|
LIGHT_DOMAIN,
|
|
SERVICE_TURN_ON,
|
|
{"entity_id": entity_id, ATTR_BRIGHTNESS: 51},
|
|
blocking=True,
|
|
)
|
|
|
|
# set_intensity called once; turn_on should not be used in this path
|
|
assert mock_light.set_intensity.await_count == 1
|
|
assert mock_light.turn_on.await_count == 0
|
|
|
|
# Inspect the intensity argument (first positional)
|
|
args, kwargs = mock_light.set_intensity.await_args
|
|
intensity_obj = args[0]
|
|
# brightness 51 -> 20% normalized -> intensity_percent = 20
|
|
assert intensity_obj.intensity_percent == 20
|
|
assert kwargs.get("wait_for_completion") is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mock_pyvlx", ["mock_light", "mock_onoff_light"], indirect=True
|
|
)
|
|
async def test_light_turn_on_without_brightness_calls_turn_on(
|
|
hass: HomeAssistant, mock_pyvlx: MagicMock
|
|
) -> None:
|
|
"""Turning on without brightness uses node.turn_on."""
|
|
|
|
node = mock_pyvlx.nodes[0]
|
|
entity_id = f"light.{node.name.lower().replace(' ', '_')}"
|
|
|
|
await hass.services.async_call(
|
|
LIGHT_DOMAIN,
|
|
SERVICE_TURN_ON,
|
|
{"entity_id": entity_id},
|
|
blocking=True,
|
|
)
|
|
|
|
node.turn_on.assert_awaited_once_with(wait_for_completion=True)
|
|
assert node.set_intensity.await_count == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mock_pyvlx", ["mock_light", "mock_onoff_light"], indirect=True
|
|
)
|
|
async def test_light_turn_off_calls_turn_off(
|
|
hass: HomeAssistant, mock_pyvlx: MagicMock
|
|
) -> None:
|
|
"""Turning off calls device.turn_off with wait_for_completion."""
|
|
|
|
node = mock_pyvlx.nodes[0]
|
|
entity_id = f"light.{node.name.lower().replace(' ', '_')}"
|
|
|
|
await hass.services.async_call(
|
|
LIGHT_DOMAIN,
|
|
SERVICE_TURN_OFF,
|
|
{"entity_id": entity_id},
|
|
blocking=True,
|
|
)
|
|
|
|
node.turn_off.assert_awaited_once_with(wait_for_completion=True)
|
|
assert node.set_intensity.await_count == 0
|