mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 00:20:30 +01:00
Casper Glow - Add Select Options (#166553)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -11,7 +11,12 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.LIGHT,
|
||||
Platform.SELECT,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
||||
|
||||
@@ -12,5 +12,7 @@ SORTED_BRIGHTNESS_LEVELS = sorted(BRIGHTNESS_LEVELS)
|
||||
|
||||
DEFAULT_DIMMING_TIME_MINUTES: int = DIMMING_TIME_MINUTES[0]
|
||||
|
||||
DIMMING_TIME_OPTIONS: tuple[str, ...] = tuple(str(m) for m in DIMMING_TIME_MINUTES)
|
||||
|
||||
# Interval between periodic state polls to catch externally-triggered changes.
|
||||
STATE_POLL_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.components.bluetooth.active_update_coordinator import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import STATE_POLL_INTERVAL
|
||||
from .const import SORTED_BRIGHTNESS_LEVELS, STATE_POLL_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +51,15 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
|
||||
)
|
||||
self.title = title
|
||||
|
||||
# The device API couples brightness and dimming time into a
|
||||
# single command (set_brightness_and_dimming_time), so both
|
||||
# values must be tracked here for cross-entity use.
|
||||
self.last_brightness_pct: int = (
|
||||
device.state.brightness_level
|
||||
if device.state.brightness_level is not None
|
||||
else SORTED_BRIGHTNESS_LEVELS[0]
|
||||
)
|
||||
|
||||
@callback
|
||||
def _needs_poll(
|
||||
self,
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
"resume": {
|
||||
"default": "mdi:play"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"dimming_time": {
|
||||
"default": "mdi:timer-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
if state.brightness_level is not None:
|
||||
self._attr_brightness = _device_pct_to_ha_brightness(state.brightness_level)
|
||||
self.coordinator.last_brightness_pct = state.brightness_level
|
||||
|
||||
@callback
|
||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
||||
@@ -97,6 +98,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
|
||||
)
|
||||
)
|
||||
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
|
||||
self.coordinator.last_brightness_pct = brightness_pct
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
|
||||
@@ -52,8 +52,10 @@ rules:
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-category: done
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: No applicable device classes for binary_sensor, button, light, or select entities.
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
|
||||
92
homeassistant/components/casper_glow/select.py
Normal file
92
homeassistant/components/casper_glow/select.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Casper Glow integration select platform for dimming time."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.const import EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import DIMMING_TIME_OPTIONS
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
from .entity import CasperGlowEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CasperGlowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the select platform for Casper Glow."""
|
||||
async_add_entities([CasperGlowDimmingTimeSelect(entry.runtime_data)])
|
||||
|
||||
|
||||
class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity):
|
||||
"""Select entity for Casper Glow dimming time."""
|
||||
|
||||
_attr_translation_key = "dimming_time"
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_options = list(DIMMING_TIME_OPTIONS)
|
||||
_attr_unit_of_measurement = UnitOfTime.MINUTES
|
||||
|
||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
||||
"""Initialize the dimming time select entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_dimming_time"
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the currently selected dimming time from the coordinator."""
|
||||
if self.coordinator.last_dimming_time_minutes is None:
|
||||
return None
|
||||
return str(self.coordinator.last_dimming_time_minutes)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last known dimming time and register state update callback."""
|
||||
await super().async_added_to_hass()
|
||||
if self.coordinator.last_dimming_time_minutes is None and (
|
||||
last_state := await self.async_get_last_state()
|
||||
):
|
||||
if last_state.state in DIMMING_TIME_OPTIONS:
|
||||
self.coordinator.last_dimming_time_minutes = int(last_state.state)
|
||||
self.async_on_remove(
|
||||
self._device.register_callback(self._async_handle_state_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
||||
"""Handle a state update from the device."""
|
||||
if state.brightness_level is not None:
|
||||
self.coordinator.last_brightness_pct = state.brightness_level
|
||||
if (
|
||||
state.configured_dimming_time_minutes is not None
|
||||
and self.coordinator.last_dimming_time_minutes is None
|
||||
):
|
||||
self.coordinator.last_dimming_time_minutes = (
|
||||
state.configured_dimming_time_minutes
|
||||
)
|
||||
# Dimming time is not part of the device state
|
||||
# that is provided via BLE update. Therefore
|
||||
# we need to trigger a state update for the select entity
|
||||
# to update the current state.
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the dimming time."""
|
||||
await self._async_command(
|
||||
self._device.set_brightness_and_dimming_time(
|
||||
self.coordinator.last_brightness_pct, int(option)
|
||||
)
|
||||
)
|
||||
self.coordinator.last_dimming_time_minutes = int(option)
|
||||
# Dimming time is not part of the device state
|
||||
# that is provided via BLE update. Therefore
|
||||
# we need to trigger a state update for the select entity
|
||||
# to update the current state.
|
||||
self.async_write_ha_state()
|
||||
@@ -39,6 +39,11 @@
|
||||
"resume": {
|
||||
"name": "Resume dimming"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"dimming_time": {
|
||||
"name": "Dimming time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Casper Glow session fixtures."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Callable, Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pycasperglow import GlowState
|
||||
@@ -51,6 +51,21 @@ def mock_casper_glow() -> Generator[MagicMock]:
|
||||
yield mock_device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fire_callbacks(
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> Callable[[GlowState], None]:
|
||||
"""Return a helper that fires all registered device callbacks with a given state."""
|
||||
|
||||
def _fire(state: GlowState) -> None:
|
||||
for cb in (
|
||||
call[0][0] for call in mock_casper_glow.register_callback.call_args_list
|
||||
):
|
||||
cb(state)
|
||||
|
||||
return _fire
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def config_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
67
tests/components/casper_glow/snapshots/test_select.ambr
Normal file
67
tests/components/casper_glow/snapshots/test_select.ambr
Normal file
@@ -0,0 +1,67 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[select.jar_dimming_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'15',
|
||||
'30',
|
||||
'45',
|
||||
'60',
|
||||
'90',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.jar_dimming_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Dimming time',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Dimming time',
|
||||
'platform': 'casper_glow',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'dimming_time',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_dimming_time',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[select.jar_dimming_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Jar Dimming time',
|
||||
'options': list([
|
||||
'15',
|
||||
'30',
|
||||
'45',
|
||||
'60',
|
||||
'90',
|
||||
]),
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.jar_dimming_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test the Casper Glow light platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pycasperglow import CasperGlowError, GlowState
|
||||
@@ -82,21 +83,19 @@ async def test_turn_off(
|
||||
async def test_state_update_via_callback(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that the entity updates state when the device fires a callback."""
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
callback = mock_casper_glow.register_callback.call_args[0][0]
|
||||
|
||||
callback(GlowState(is_on=True))
|
||||
fire_callbacks(GlowState(is_on=True))
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
callback(GlowState(is_on=False))
|
||||
fire_callbacks(GlowState(is_on=False))
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
@@ -115,25 +114,6 @@ async def test_color_mode(
|
||||
assert ColorMode.BRIGHTNESS in state.attributes["supported_color_modes"]
|
||||
|
||||
|
||||
async def test_turn_on_with_brightness(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> None:
|
||||
"""Test turning on the light with brightness."""
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 255},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_casper_glow.turn_on.assert_called_once_with()
|
||||
mock_casper_glow.set_brightness_and_dimming_time.assert_called_once_with(
|
||||
100, DEFAULT_DIMMING_TIME_MINUTES
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ha_brightness", "device_pct"),
|
||||
[
|
||||
@@ -169,11 +149,10 @@ async def test_brightness_snap_to_nearest(
|
||||
async def test_brightness_update_via_callback(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that brightness updates via device callback."""
|
||||
callback = mock_casper_glow.register_callback.call_args[0][0]
|
||||
callback(GlowState(is_on=True, brightness_level=80))
|
||||
fire_callbacks(GlowState(is_on=True, brightness_level=80))
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
@@ -181,18 +160,29 @@ async def test_brightness_update_via_callback(
|
||||
assert state.attributes.get(ATTR_BRIGHTNESS) == 153
|
||||
|
||||
|
||||
async def test_turn_on_error(
|
||||
@pytest.mark.usefixtures("config_entry")
|
||||
@pytest.mark.parametrize(
|
||||
("service", "mock_method"),
|
||||
[
|
||||
(SERVICE_TURN_ON, "turn_on"),
|
||||
(SERVICE_TURN_OFF, "turn_off"),
|
||||
],
|
||||
)
|
||||
async def test_command_error(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
service: str,
|
||||
mock_method: str,
|
||||
) -> None:
|
||||
"""Test that a turn on error raises HomeAssistantError without marking entity unavailable."""
|
||||
mock_casper_glow.turn_on.side_effect = CasperGlowError("Connection failed")
|
||||
"""Test that a device error raises HomeAssistantError without marking entity unavailable."""
|
||||
getattr(mock_casper_glow, mock_method).side_effect = CasperGlowError(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
@@ -202,27 +192,11 @@ async def test_turn_on_error(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_turn_off_error(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that a turn off error raises HomeAssistantError."""
|
||||
mock_casper_glow.turn_off.side_effect = CasperGlowError("Connection failed")
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_state_update_via_callback_after_command_failure(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that device callbacks correctly update state even after a command failure."""
|
||||
mock_casper_glow.turn_on.side_effect = CasperGlowError("Connection failed")
|
||||
@@ -241,8 +215,7 @@ async def test_state_update_via_callback_after_command_failure(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Device sends a push state update — entity reflects true device state
|
||||
callback = mock_casper_glow.register_callback.call_args[0][0]
|
||||
callback(GlowState(is_on=True))
|
||||
fire_callbacks(GlowState(is_on=True))
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
|
||||
199
tests/components/casper_glow/test_select.py
Normal file
199
tests/components/casper_glow/test_select.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""Test the Casper Glow select platform."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pycasperglow import CasperGlowError, GlowState
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.casper_glow.const import (
|
||||
DIMMING_TIME_OPTIONS,
|
||||
SORTED_BRIGHTNESS_LEVELS,
|
||||
)
|
||||
from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, mock_restore_cache, snapshot_platform
|
||||
|
||||
ENTITY_ID = "select.jar_dimming_time"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test all select entities match the snapshot."""
|
||||
with patch("homeassistant.components.casper_glow.PLATFORMS", [Platform.SELECT]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_select_state_from_callback(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that the select entity shows dimming time reported by device callback."""
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
fire_callbacks(
|
||||
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[2]))
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[2]
|
||||
|
||||
|
||||
async def test_select_option(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test selecting a dimming time option."""
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, "option": DIMMING_TIME_OPTIONS[1]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_casper_glow.set_brightness_and_dimming_time.assert_called_once_with(
|
||||
SORTED_BRIGHTNESS_LEVELS[0], int(DIMMING_TIME_OPTIONS[1])
|
||||
)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[1]
|
||||
|
||||
# A subsequent device callback must not overwrite the user's selection.
|
||||
fire_callbacks(
|
||||
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[0]))
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[1]
|
||||
|
||||
|
||||
async def test_select_option_error(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that a set_brightness_and_dimming_time error raises HomeAssistantError."""
|
||||
mock_casper_glow.set_brightness_and_dimming_time.side_effect = CasperGlowError(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, "option": DIMMING_TIME_OPTIONS[1]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_select_state_update_via_callback_after_command_failure(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that device callbacks correctly update state even after a command failure."""
|
||||
mock_casper_glow.set_brightness_and_dimming_time.side_effect = CasperGlowError(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, "option": DIMMING_TIME_OPTIONS[1]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Device sends a push state update — entity reflects true state
|
||||
fire_callbacks(
|
||||
GlowState(configured_dimming_time_minutes=int(DIMMING_TIME_OPTIONS[1]))
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[1]
|
||||
|
||||
|
||||
async def test_select_ignores_remaining_time_updates(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
fire_callbacks: Callable[[GlowState], None],
|
||||
) -> None:
|
||||
"""Test that callbacks with only remaining time do not change the select state."""
|
||||
fire_callbacks(GlowState(dimming_time_minutes=44))
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_restore_state(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the dimming time is restored from the last known state on restart."""
|
||||
mock_restore_cache(hass, (State(ENTITY_ID, DIMMING_TIME_OPTIONS[3]),))
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == DIMMING_TIME_OPTIONS[3]
|
||||
|
||||
# Coordinator should be seeded with the restored value.
|
||||
assert mock_config_entry.runtime_data.last_dimming_time_minutes == int(
|
||||
DIMMING_TIME_OPTIONS[3]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"restored_state",
|
||||
[STATE_UNKNOWN, "invalid", "999"],
|
||||
)
|
||||
async def test_restore_state_ignores_invalid(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_casper_glow: MagicMock,
|
||||
restored_state: str,
|
||||
) -> None:
|
||||
"""Test that invalid or unsupported restored states are ignored."""
|
||||
mock_restore_cache(hass, (State(ENTITY_ID, restored_state),))
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert mock_config_entry.runtime_data.last_dimming_time_minutes is None
|
||||
Reference in New Issue
Block a user