mirror of
https://github.com/home-assistant/core.git
synced 2025-11-14 05:20:17 +00:00
850 lines
28 KiB
Python
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"
|