Files
core/tests/components/openrgb/test_light.py
2025-10-24 16:19:45 +02:00

850 lines
28 KiB
Python

"""Tests for the OpenRGB light platform."""
from collections.abc import Generator
import copy
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from openrgb.utils import OpenRGBDisconnected, RGBColor
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_MODE,
ATTR_EFFECT,
ATTR_RGB_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
DOMAIN as LIGHT_DOMAIN,
EFFECT_OFF,
ColorMode,
LightEntityFeature,
)
from homeassistant.components.openrgb.const import (
DEFAULT_COLOR,
DOMAIN,
OFF_COLOR,
SCAN_INTERVAL,
OpenRGBMode,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
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 light_only() -> Generator[None]:
"""Enable only the light platform."""
with patch(
"homeassistant.components.openrgb.PLATFORMS",
[Platform.LIGHT],
):
yield
# 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 light entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
# Ensure entities are correctly assigned to device
device_entry = device_registry.async_get_device(
identifiers={
(
DOMAIN,
f"{mock_config_entry.entry_id}||DRAM||ENE||ENE SMBus Device||none||I2C: PIIX4, address 0x70",
)
}
)
assert device_entry
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
# Filter out the server device
entity_entries = [e for e in entity_entries if e.device_id == device_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_light_with_black_leds(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
) -> None:
"""Test light state when all LEDs are black (off by color)."""
# Set all LEDs to black
mock_openrgb_device.colors = [RGBColor(*OFF_COLOR), RGBColor(*OFF_COLOR)]
mock_openrgb_device.active_mode = 0 # Direct mode (supports colors)
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 light is off by color
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_OFF
assert state.attributes.get(ATTR_RGB_COLOR) is None
assert state.attributes.get(ATTR_BRIGHTNESS) is None
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_light_with_one_non_black_led(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
) -> None:
"""Test light state when one LED is non-black among black LEDs (on by color)."""
# Set one LED to red, others to black
mock_openrgb_device.colors = [RGBColor(*OFF_COLOR), RGBColor(255, 0, 0)]
mock_openrgb_device.active_mode = 0 # Direct mode (supports colors)
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 light is on with the non-black LED color
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
assert state.attributes.get(ATTR_COLOR_MODE) == ColorMode.RGB
assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ColorMode.RGB]
assert state.attributes.get(ATTR_RGB_COLOR) == (255, 0, 0)
assert state.attributes.get(ATTR_BRIGHTNESS) == 255
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_light_with_non_color_mode(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
) -> None:
"""Test light state with a mode that doesn't support colors."""
# Set to Rainbow mode (doesn't support colors)
mock_openrgb_device.active_mode = 6
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 light is on with ON/OFF mode
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == LightEntityFeature.EFFECT
assert state.attributes.get(ATTR_EFFECT) == "rainbow"
assert state.attributes.get(ATTR_COLOR_MODE) == ColorMode.ONOFF
assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ColorMode.ONOFF]
assert state.attributes.get(ATTR_RGB_COLOR) is None
assert state.attributes.get(ATTR_BRIGHTNESS) is None
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_light_with_no_effects(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
) -> None:
"""Test light with a device that has no effects."""
# Keep only no-effect modes in the device
mock_openrgb_device.modes = [
mode
for mode in mock_openrgb_device.modes
if mode.name in {OpenRGBMode.OFF, OpenRGBMode.DIRECT, OpenRGBMode.STATIC}
]
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 light entity doesn't have EFFECT feature
state = hass.states.get("light.ene_dram")
assert state
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0
assert state.attributes.get(ATTR_EFFECT) is None
# Verify the light is still functional (can be turned on/off)
assert state.state == STATE_ON
# Test basic turn on/off functionality
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_turn_on_light(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
) -> None:
"""Test turning on the light."""
# Initialize device in Off mode
mock_openrgb_device.active_mode = 1
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 light is initially off
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_OFF
# Turn on without parameters
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.ene_dram"},
blocking=True,
)
# Verify that set_mode was called to restore to Direct mode (preferred over Static)
mock_openrgb_device.set_mode.assert_called_once_with(OpenRGBMode.DIRECT)
# And set_color was called with default color
mock_openrgb_device.set_color.assert_called_once_with(
RGBColor(*DEFAULT_COLOR), True
)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_turn_on_light_with_color(
hass: HomeAssistant,
mock_openrgb_device: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test turning on the light with color."""
# Start with color Red at half brightness
mock_openrgb_device.colors = [RGBColor(128, 0, 0), RGBColor(128, 0, 0)]
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 state
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
assert state.attributes.get(ATTR_RGB_COLOR) == (255, 0, 0) # Red
assert state.attributes.get(ATTR_BRIGHTNESS) == 128
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_RGB_COLOR: (0, 255, 0), # Green
},
blocking=True,
)
# Check that set_color was called with Green color scaled with half brightness
mock_openrgb_device.set_color.assert_called_once_with(RGBColor(0, 128, 0), True)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_turn_on_light_with_brightness(
hass: HomeAssistant,
mock_openrgb_device: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test turning on the light with brightness."""
# Start with color Red at full brightness
mock_openrgb_device.colors = [RGBColor(255, 0, 0), RGBColor(255, 0, 0)]
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 state
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
assert state.attributes.get(ATTR_RGB_COLOR) == (255, 0, 0) # Red
assert state.attributes.get(ATTR_BRIGHTNESS) == 255
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_BRIGHTNESS: 128,
},
blocking=True,
)
# Check that set_color was called with Red color scaled with half brightness
mock_openrgb_device.set_color.assert_called_once_with(RGBColor(128, 0, 0), True)
@pytest.mark.usefixtures("init_integration")
async def test_turn_on_light_with_effect(
hass: HomeAssistant,
mock_openrgb_device: MagicMock,
) -> None:
"""Test turning on the light with effect."""
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_EFFECT: "rainbow",
},
blocking=True,
)
mock_openrgb_device.set_mode.assert_called_once_with("Rainbow")
@pytest.mark.usefixtures("init_integration")
async def test_turn_on_light_with_effect_off(
hass: HomeAssistant,
mock_openrgb_device: MagicMock,
) -> None:
"""Test turning on the light with effect Off."""
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_EFFECT: EFFECT_OFF,
},
blocking=True,
)
# Should switch to Direct mode (preferred over Static)
mock_openrgb_device.set_mode.assert_called_once_with(OpenRGBMode.DIRECT)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_turn_on_restores_previous_values(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test turning on after off restores previous brightness, color, and mode."""
# Start with device in Static mode with blue color
mock_openrgb_device.active_mode = 2
mock_openrgb_device.colors = [RGBColor(0, 0, 128), RGBColor(0, 0, 128)]
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 state
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
# Now device is in Off mode
mock_openrgb_device.active_mode = 1
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_OFF
# Turn on without parameters - should restore previous mode and values
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.ene_dram"},
blocking=True,
)
# Should restore to Static mode (previous mode) even though Direct is preferred
mock_openrgb_device.set_mode.assert_called_once_with(OpenRGBMode.STATIC)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_previous_values_updated_on_refresh(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that previous values are updated when device state changes externally."""
# Start with device in Direct mode with red color at full brightness
mock_openrgb_device.active_mode = 0
mock_openrgb_device.colors = [RGBColor(255, 0, 0), RGBColor(255, 0, 0)]
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 state
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
assert state.attributes.get(ATTR_RGB_COLOR) == (255, 0, 0) # Red
assert state.attributes.get(ATTR_BRIGHTNESS) == 255
assert state.attributes.get(ATTR_EFFECT) == EFFECT_OFF # Direct mode
# Simulate external change to green at 50% brightness in Breathing mode
# (e.g., via the OpenRGB application)
mock_openrgb_device.active_mode = 3 # Breathing mode
mock_openrgb_device.colors = [RGBColor(0, 128, 0), RGBColor(0, 128, 0)]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Verify new state
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
assert state.attributes.get(ATTR_RGB_COLOR) == (0, 255, 0) # Green
assert state.attributes.get(ATTR_BRIGHTNESS) == 128 # 50% brightness
assert state.attributes.get(ATTR_EFFECT) == "breathing"
# Simulate external change to Off mode
mock_openrgb_device.active_mode = 1
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Verify light is off
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_OFF
# Turn on without parameters - should restore most recent state (green, 50%, Breathing)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.ene_dram"},
blocking=True,
)
mock_openrgb_device.set_mode.assert_called_once_with("Breathing")
mock_openrgb_device.set_color.assert_called_once_with(RGBColor(0, 128, 0), True)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_turn_on_restores_rainbow_after_off(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test turning on after off restores Rainbow effect (non-color mode)."""
# Start with device in Rainbow mode (doesn't support colors)
mock_openrgb_device.active_mode = 6
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 state - Rainbow mode active
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
assert state.attributes.get(ATTR_EFFECT) == "rainbow"
# Turn off the light by switching to Off mode
mock_openrgb_device.active_mode = 1
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Verify light is off
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_OFF
# Turn on without parameters - should restore Rainbow mode
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.ene_dram"},
blocking=True,
)
# Should restore to Rainbow mode (previous mode)
mock_openrgb_device.set_mode.assert_called_once_with("Rainbow")
# set_color should NOT be called since Rainbow doesn't support colors
mock_openrgb_device.set_color.assert_not_called()
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_turn_on_restores_rainbow_after_off_by_color(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test turning on after off by color restores Rainbow effect (non-color mode)."""
# Start with device in Rainbow mode (doesn't support colors)
mock_openrgb_device.active_mode = 6
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 state - Rainbow mode active
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
assert state.attributes.get(ATTR_EFFECT) == "rainbow"
# Turn off the light by setting all LEDs to black in Direct mode
mock_openrgb_device.active_mode = 0 # Direct mode
mock_openrgb_device.colors = [RGBColor(*OFF_COLOR), RGBColor(*OFF_COLOR)]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Verify light is off
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_OFF
# Turn on without parameters - should restore Rainbow mode, not Direct
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.ene_dram"},
blocking=True,
)
# Should restore to Rainbow mode (previous mode), not Direct
mock_openrgb_device.set_mode.assert_called_once_with("Rainbow")
# set_color should NOT be called since Rainbow doesn't support colors
mock_openrgb_device.set_color.assert_not_called()
@pytest.mark.usefixtures("init_integration")
async def test_turn_off_light(
hass: HomeAssistant,
mock_openrgb_device: MagicMock,
) -> None:
"""Test turning off the light."""
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.ene_dram"},
blocking=True,
)
# Device supports "Off" mode
mock_openrgb_device.set_mode.assert_called_once_with(OpenRGBMode.OFF)
@pytest.mark.usefixtures("mock_openrgb_client")
async def test_turn_off_light_without_off_mode(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_device: MagicMock,
) -> None:
"""Test turning off a light that doesn't support Off mode."""
# Modify the device to not have Off mode
mock_openrgb_device.modes = [
mode_data
for mode_data in mock_openrgb_device.modes
if mode_data.name != OpenRGBMode.OFF
]
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 light is initially on
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
# Turn off the light
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.ene_dram"},
blocking=True,
)
# Device should have set_color called with black/off color instead
mock_openrgb_device.set_color.assert_called_once_with(RGBColor(*OFF_COLOR), True)
# Test error handling
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
"exception",
[OpenRGBDisconnected(), ValueError("Invalid color")],
)
async def test_turn_on_light_with_color_exceptions(
hass: HomeAssistant,
mock_openrgb_device: MagicMock,
exception: Exception,
) -> None:
"""Test turning on the light with exceptions when setting color."""
mock_openrgb_device.set_color.side_effect = exception
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_RGB_COLOR: (0, 255, 0),
},
blocking=True,
)
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
"exception",
[OpenRGBDisconnected(), ValueError("Invalid mode")],
)
async def test_turn_on_light_with_mode_exceptions(
hass: HomeAssistant,
mock_openrgb_device: MagicMock,
exception: Exception,
) -> None:
"""Test turning on the light with exceptions when setting mode."""
mock_openrgb_device.set_mode.side_effect = exception
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_EFFECT: "rainbow",
},
blocking=True,
)
@pytest.mark.usefixtures("init_integration")
async def test_turn_on_light_with_unsupported_effect(
hass: HomeAssistant,
) -> None:
"""Test turning on the light with an invalid effect."""
with pytest.raises(
ServiceValidationError,
match="Effect `InvalidEffect` is not supported by ENE DRAM",
):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_EFFECT: "InvalidEffect",
},
blocking=True,
)
@pytest.mark.usefixtures("init_integration")
async def test_turn_on_light_with_color_and_non_color_effect(
hass: HomeAssistant,
) -> None:
"""Test turning on the light with color/brightness and a non-color effect."""
with pytest.raises(
ServiceValidationError,
match="Effect `rainbow` does not support color control on ENE DRAM",
):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_EFFECT: "rainbow",
ATTR_RGB_COLOR: (255, 0, 0),
},
blocking=True,
)
@pytest.mark.usefixtures("init_integration")
async def test_turn_on_light_with_brightness_and_non_color_effect(
hass: HomeAssistant,
) -> None:
"""Test turning on the light with brightness and a non-color effect."""
with pytest.raises(
ServiceValidationError,
match="Effect `rainbow` does not support color control on ENE DRAM",
):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.ene_dram",
ATTR_EFFECT: "rainbow",
ATTR_BRIGHTNESS: 128,
},
blocking=True,
)
# Test device management
async def test_dynamic_device_addition(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_openrgb_device: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that new devices are added dynamically."""
mock_config_entry.add_to_hass(hass)
# Start with one device
mock_openrgb_client.devices = [mock_openrgb_device]
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
# Check that one light entity exists
state = hass.states.get("light.ene_dram")
assert state
# Add a second device
new_device = MagicMock()
new_device.id = 1 # Different device ID
new_device.name = "New RGB Device"
new_device.type = MagicMock()
new_device.type.name = "KEYBOARD"
new_device.metadata = MagicMock()
new_device.metadata.vendor = "New Vendor"
new_device.metadata.description = "New Keyboard"
new_device.metadata.serial = "NEW123"
new_device.metadata.location = "New Location"
new_device.metadata.version = "2.0.0"
new_device.active_mode = 0
new_device.modes = mock_openrgb_device.modes
new_device.colors = [RGBColor(0, 255, 0)]
new_device.set_color = MagicMock()
new_device.set_mode = MagicMock()
mock_openrgb_client.devices = [mock_openrgb_device, new_device]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Check that second light entity was added
state = hass.states.get("light.new_rgb_device")
assert state
@pytest.mark.usefixtures("init_integration")
async def test_light_availability(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test light becomes unavailable when device is unplugged."""
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_ON
# Simulate device being momentarily unplugged
mock_openrgb_client.devices = []
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get("light.ene_dram")
assert state
assert state.state == STATE_UNAVAILABLE
async def test_duplicate_device_names(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_openrgb_client: MagicMock,
mock_openrgb_device: MagicMock,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test that devices with duplicate names get numeric suffixes."""
device1 = copy.deepcopy(mock_openrgb_device)
device1.id = 3 # Should get suffix "1"
device1.metadata.location = "I2C: PIIX4, address 0x71"
# Create a true copy of the first device for device2 to ensure they are separate instances
device2 = copy.deepcopy(mock_openrgb_device)
device2.id = 4 # Should get suffix "2"
device2.metadata.location = "I2C: PIIX4, address 0x72"
mock_openrgb_client.devices = [device1, device2]
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
# The device key format is: entry_id||type||vendor||description||serial||location
device1_key = f"{mock_config_entry.entry_id}||DRAM||ENE||ENE SMBus Device||none||I2C: PIIX4, address 0x71"
device2_key = f"{mock_config_entry.entry_id}||DRAM||ENE||ENE SMBus Device||none||I2C: PIIX4, address 0x72"
# Verify devices exist with correct names (suffix based on device.id position)
device1_entry = device_registry.async_get_device(
identifiers={(DOMAIN, device1_key)}
)
device2_entry = device_registry.async_get_device(
identifiers={(DOMAIN, device2_key)}
)
assert device1_entry
assert device2_entry
# device1 has lower device.id, so it gets suffix "1"
# device2 has higher device.id, so it gets suffix "2"
assert device1_entry.name == "ENE DRAM 1"
assert device2_entry.name == "ENE DRAM 2"