mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Add mode selector to Twinkly (#134041)
This commit is contained in:
parent
add4e1a708
commit
0b32342bf0
@ -14,7 +14,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import TwinklyCoordinator
|
from .coordinator import TwinklyCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.LIGHT]
|
PLATFORMS = [Platform.LIGHT, Platform.SELECT]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ class TwinklyData:
|
|||||||
is_on: bool
|
is_on: bool
|
||||||
movies: dict[int, str]
|
movies: dict[int, str]
|
||||||
current_movie: int | None
|
current_movie: int | None
|
||||||
|
current_mode: str | None
|
||||||
|
|
||||||
|
|
||||||
class TwinklyCoordinator(DataUpdateCoordinator[TwinklyData]):
|
class TwinklyCoordinator(DataUpdateCoordinator[TwinklyData]):
|
||||||
@ -66,6 +67,8 @@ class TwinklyCoordinator(DataUpdateCoordinator[TwinklyData]):
|
|||||||
device_info = await self.client.get_details()
|
device_info = await self.client.get_details()
|
||||||
brightness = await self.client.get_brightness()
|
brightness = await self.client.get_brightness()
|
||||||
is_on = await self.client.is_on()
|
is_on = await self.client.is_on()
|
||||||
|
mode_data = await self.client.get_mode()
|
||||||
|
current_mode = mode_data.get("mode")
|
||||||
if self.supports_effects:
|
if self.supports_effects:
|
||||||
movies = (await self.client.get_saved_movies())["movies"]
|
movies = (await self.client.get_saved_movies())["movies"]
|
||||||
except (TimeoutError, ClientError) as exception:
|
except (TimeoutError, ClientError) as exception:
|
||||||
@ -87,6 +90,7 @@ class TwinklyCoordinator(DataUpdateCoordinator[TwinklyData]):
|
|||||||
is_on,
|
is_on,
|
||||||
{movie["id"]: movie["name"] for movie in movies},
|
{movie["id"]: movie["name"] for movie in movies},
|
||||||
current_movie.get("id"),
|
current_movie.get("id"),
|
||||||
|
current_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _async_update_device_info(self, name: str) -> None:
|
def _async_update_device_info(self, name: str) -> None:
|
||||||
|
49
homeassistant/components/twinkly/select.py
Normal file
49
homeassistant/components/twinkly/select.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""The Twinkly select component."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ttls.client import TWINKLY_MODES
|
||||||
|
|
||||||
|
from homeassistant.components.select import SelectEntity
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import TwinklyConfigEntry, TwinklyCoordinator
|
||||||
|
from .entity import TwinklyEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: TwinklyConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up a mode select from a config entry."""
|
||||||
|
entity = TwinklyModeSelect(config_entry.runtime_data)
|
||||||
|
async_add_entities([entity], update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TwinklyModeSelect(TwinklyEntity, SelectEntity):
|
||||||
|
"""Twinkly Mode Selection."""
|
||||||
|
|
||||||
|
_attr_name = "Mode"
|
||||||
|
_attr_options = TWINKLY_MODES
|
||||||
|
|
||||||
|
def __init__(self, coordinator: TwinklyCoordinator) -> None:
|
||||||
|
"""Initialize TwinklyModeSelect."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_unique_id = f"{coordinator.data.device_info["mac"]}_mode"
|
||||||
|
self.client = coordinator.client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Return current mode."""
|
||||||
|
return self.coordinator.data.current_mode
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
await self.client.set_mode(option)
|
||||||
|
await self.coordinator.async_refresh()
|
@ -57,6 +57,7 @@ def mock_twinkly_client() -> Generator[AsyncMock]:
|
|||||||
client.get_current_movie.return_value = load_json_object_fixture(
|
client.get_current_movie.return_value = load_json_object_fixture(
|
||||||
"get_current_movie.json", DOMAIN
|
"get_current_movie.json", DOMAIN
|
||||||
)
|
)
|
||||||
|
client.get_mode.return_value = load_json_object_fixture("get_mode.json", DOMAIN)
|
||||||
client.is_on.return_value = True
|
client.is_on.return_value = True
|
||||||
client.get_brightness.return_value = {"mode": "enabled", "value": 10}
|
client.get_brightness.return_value = {"mode": "enabled", "value": 10}
|
||||||
client.host = "192.168.0.123"
|
client.host = "192.168.0.123"
|
||||||
|
3
tests/components/twinkly/fixtures/get_mode.json
Normal file
3
tests/components/twinkly/fixtures/get_mode.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"mode": "color"
|
||||||
|
}
|
66
tests/components/twinkly/snapshots/test_select.ambr
Normal file
66
tests/components/twinkly/snapshots/test_select.ambr
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_select_entities[select.tree_1_mode-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'options': list([
|
||||||
|
'color',
|
||||||
|
'demo',
|
||||||
|
'effect',
|
||||||
|
'movie',
|
||||||
|
'off',
|
||||||
|
'playlist',
|
||||||
|
'rt',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'select',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'select.tree_1_mode',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Mode',
|
||||||
|
'platform': 'twinkly',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '00:2d:13:3b:aa:bb_mode',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_select_entities[select.tree_1_mode-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Tree 1 Mode',
|
||||||
|
'options': list([
|
||||||
|
'color',
|
||||||
|
'demo',
|
||||||
|
'effect',
|
||||||
|
'movie',
|
||||||
|
'off',
|
||||||
|
'playlist',
|
||||||
|
'rt',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'select.tree_1_mode',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'color',
|
||||||
|
})
|
||||||
|
# ---
|
77
tests/components/twinkly/test_select.py
Normal file
77
tests/components/twinkly/test_select.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"""Tests for the Twinkly select component."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, STATE_UNAVAILABLE, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_twinkly_client")
|
||||||
|
async def test_select_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test the created select entities."""
|
||||||
|
with patch("homeassistant.components.twinkly.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_mode(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_twinkly_client: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test selecting a mode."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
state = hass.states.get("select.tree_1_mode")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == "color"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
"select_option",
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: "select.tree_1_mode",
|
||||||
|
ATTR_OPTION: "movie",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_twinkly_client.set_mode.assert_called_once_with("movie")
|
||||||
|
mock_twinkly_client.interview.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mode_unavailable(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_twinkly_client: AsyncMock,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test handling of unavailable mode data."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
mock_twinkly_client.get_mode.side_effect = Exception
|
||||||
|
freezer.tick(timedelta(seconds=30))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("select.tree_1_mode")
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
Loading…
x
Reference in New Issue
Block a user