Files
core/tests/components/openrgb/test_select.py
2025-12-29 17:42:20 +01:00

419 lines
14 KiB
Python

"""Tests for the OpenRGB select platform."""
from collections.abc import Generator
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from openrgb.utils import OpenRGBDisconnected
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.openrgb.const import DOMAIN, SCAN_INTERVAL
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_OPTION,
SERVICE_SELECT_OPTION,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.fixture(autouse=True)
def select_only() -> Generator[None]:
"""Enable only the select platform."""
with patch(
"homeassistant.components.openrgb.PLATFORMS",
[Platform.SELECT],
):
yield
@pytest.fixture
def mock_profiles() -> list[SimpleNamespace]:
"""Return a list of mock profiles."""
return [
SimpleNamespace(name="Gaming"),
SimpleNamespace(name="Work"),
SimpleNamespace(name="Rainbow"),
]
# Test basic entity setup and configuration
@pytest.mark.usefixtures("init_integration")
async def test_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the select entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure entity is correctly assigned to the OpenRGB server device
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, mock_config_entry.entry_id)}
)
assert device_entry
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert len(entity_entries) == 1
assert entity_entries[0].device_id == device_entry.id
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_select_with_profiles(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_profiles: list[SimpleNamespace],
) -> None:
"""Test select entity with available profiles."""
mock_openrgb_client.profiles = mock_profiles
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Verify select entity has profile options
state = hass.states.get("select.test_computer_profile")
assert state
assert state.attributes.get("options") == ["Gaming", "Work", "Rainbow"]
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_select_with_no_profiles(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
) -> None:
"""Test select entity when no profiles are available."""
mock_openrgb_client.profiles = []
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Verify select entity is unavailable when no profiles exist
state = hass.states.get("select.test_computer_profile")
assert state
assert state.state == STATE_UNAVAILABLE
assert state.attributes.get("options") == []
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_select_option_success(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_profiles: list[SimpleNamespace],
) -> None:
"""Test selecting a profile successfully."""
mock_openrgb_client.profiles = mock_profiles
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Select a profile
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.test_computer_profile",
ATTR_OPTION: "Gaming",
},
blocking=True,
)
# Verify load_profile was called with the correct profile name
mock_openrgb_client.load_profile.assert_called_once_with("Gaming")
# Verify the current option is set to the selected profile
state = hass.states.get("select.test_computer_profile")
assert state
assert state.state == "Gaming"
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_select_option_not_found(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_profiles: list[SimpleNamespace],
) -> None:
"""Test selecting a profile that doesn't exist."""
mock_openrgb_client.profiles = mock_profiles
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Try to select a non-existent profile
# The select platform will validate that the option is in the options list
with pytest.raises(
ServiceValidationError,
match="Option NonExistent is not valid for entity select.test_computer_profile",
):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.test_computer_profile",
ATTR_OPTION: "NonExistent",
},
blocking=True,
)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_select_option_connection_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_profiles: list[SimpleNamespace],
) -> None:
"""Test selecting a profile with connection error."""
mock_openrgb_client.profiles = mock_profiles
mock_openrgb_client.load_profile.side_effect = OpenRGBDisconnected(
"Connection lost"
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Try to select a profile - should raise HomeAssistantError
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.test_computer_profile",
ATTR_OPTION: "Gaming",
},
blocking=True,
)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_select_option_value_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_profiles: list[SimpleNamespace],
) -> None:
"""Test selecting a profile with ValueError."""
mock_openrgb_client.profiles = mock_profiles
mock_openrgb_client.load_profile.side_effect = ValueError("Invalid profile data")
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Try to select a profile - should raise HomeAssistantError
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.test_computer_profile",
ATTR_OPTION: "Gaming",
},
blocking=True,
)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_profiles_update_on_refresh(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_profiles: list[SimpleNamespace],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that profile list updates when profiles change."""
# Start with initial profiles
mock_openrgb_client.profiles = mock_profiles[:2] # Only Gaming and Work
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Verify initial profile options
state = hass.states.get("select.test_computer_profile")
assert state
assert state.attributes.get("options") == ["Gaming", "Work"]
# Add a new profile
mock_openrgb_client.profiles = mock_profiles # All three profiles
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify updated profile options
state = hass.states.get("select.test_computer_profile")
assert state
assert state.attributes.get("options") == ["Gaming", "Work", "Rainbow"]
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_select_becomes_unavailable_when_profiles_removed(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_profiles: list[SimpleNamespace],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test select becomes unavailable when all profiles are removed."""
# Start with profiles
mock_openrgb_client.profiles = mock_profiles
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Verify select entity is available with profiles
state = hass.states.get("select.test_computer_profile")
assert state
assert state.state != STATE_UNAVAILABLE
assert state.attributes.get("options") == ["Gaming", "Work", "Rainbow"]
# Remove all profiles
mock_openrgb_client.profiles = []
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify select entity becomes unavailable
state = hass.states.get("select.test_computer_profile")
assert state
assert state.state == STATE_UNAVAILABLE
assert state.attributes.get("options") == []
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_current_option_cleared_when_device_state_changes(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_openrgb_device: MagicMock,
mock_profiles: list[SimpleNamespace],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test current option is cleared when device state changes externally."""
mock_openrgb_client.profiles = mock_profiles
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Select a profile
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.test_computer_profile",
ATTR_OPTION: "Gaming",
},
blocking=True,
)
# Verify the current option is set
state = hass.states.get("select.test_computer_profile")
assert state
assert state.state == "Gaming"
# Simulate external device state change by modifying active_mode
mock_openrgb_device.active_mode = 0
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify current option is cleared because device state changed
state = hass.states.get("select.test_computer_profile")
assert state
assert state.state == "unknown"
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_current_option_cleared_when_colors_change(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_openrgb_device: MagicMock,
mock_profiles: list[SimpleNamespace],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test current option is cleared when device colors change externally."""
mock_openrgb_client.profiles = mock_profiles
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
# Select a profile
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.test_computer_profile",
ATTR_OPTION: "Work",
},
blocking=True,
)
# Verify the current option is set
state = hass.states.get("select.test_computer_profile")
assert state
assert state.state == "Work"
# Simulate external color change
mock_openrgb_device.colors[0].red = 0
mock_openrgb_device.colors[0].green = 255
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
await hass.async_block_till_done()
# Verify current option is cleared because device colors changed
state = hass.states.get("select.test_computer_profile")
assert state
assert state.state == "unknown"