Refactor Twinkly tests (#133725)

This commit is contained in:
Joost Lekkerkerker 2024-12-22 12:00:24 +01:00 committed by GitHub
parent 31c6443a9b
commit 7be3cad1db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 575 additions and 623 deletions

View File

@ -17,7 +17,7 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"device_exists": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -1,120 +1,13 @@
"""Constants and mock for the twinkly component tests."""
from aiohttp.client_exceptions import ClientConnectionError
from homeassistant.core import HomeAssistant
from homeassistant.components.twinkly.const import DEV_NAME
TEST_HOST = "test.twinkly.com"
TEST_ID = "twinkly_test_device_id"
TEST_UID = "4c8fccf5-e08a-4173-92d5-49bf479252a2"
TEST_MAC = "aa:bb:cc:dd:ee:ff"
TEST_NAME = "twinkly_test_device_name"
TEST_NAME_ORIGINAL = "twinkly_test_original_device_name" # the original (deprecated) name stored in the conf
TEST_MODEL = "twinkly_test_device_model"
from tests.common import MockConfigEntry
class ClientMock:
"""A mock of the ttls.client.Twinkly."""
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
def __init__(self) -> None:
"""Create a mocked client."""
self.is_offline = False
self.state = True
self.brightness = {"mode": "enabled", "value": 10}
self.color = None
self.movies = [{"id": 1, "name": "Rainbow"}, {"id": 2, "name": "Flare"}]
self.current_movie = {}
self.default_mode = "movie"
self.mode = None
self.version = "2.8.10"
self.id = TEST_UID
self.device_info = {
"uuid": self.id,
"device_name": TEST_NAME,
"mac": TEST_MAC,
"product_code": TEST_MODEL,
}
@property
def host(self) -> str:
"""Get the mocked host."""
return TEST_HOST
async def get_details(self):
"""Get the mocked device info."""
if self.is_offline:
raise ClientConnectionError
return self.device_info
async def is_on(self) -> bool:
"""Get the mocked on/off state."""
if self.is_offline:
raise ClientConnectionError
return self.state
async def turn_on(self) -> None:
"""Set the mocked on state."""
if self.is_offline:
raise ClientConnectionError
self.state = True
self.mode = self.default_mode
async def turn_off(self) -> None:
"""Set the mocked off state."""
if self.is_offline:
raise ClientConnectionError
self.state = False
async def get_brightness(self) -> int:
"""Get the mocked brightness."""
if self.is_offline:
raise ClientConnectionError
return self.brightness
async def set_brightness(self, brightness: int) -> None:
"""Set the mocked brightness."""
if self.is_offline:
raise ClientConnectionError
self.brightness = {"mode": "enabled", "value": brightness}
def change_name(self, new_name: str) -> None:
"""Change the name of this virtual device."""
self.device_info[DEV_NAME] = new_name
async def set_static_colour(self, colour) -> None:
"""Set static color."""
self.color = colour
self.default_mode = "color"
async def set_cycle_colours(self, colour) -> None:
"""Set static color."""
self.color = colour
self.default_mode = "movie"
async def interview(self) -> None:
"""Interview."""
async def get_saved_movies(self) -> dict:
"""Get saved movies."""
return self.movies
async def get_current_movie(self) -> dict:
"""Get current movie."""
return self.current_movie
async def set_current_movie(self, movie_id: int) -> dict:
"""Set current movie."""
self.current_movie = {"id": movie_id}
async def set_mode(self, mode: str) -> None:
"""Set mode."""
if mode == "off":
await self.turn_off()
else:
await self.turn_on()
self.mode = mode
async def get_firmware_version(self) -> dict:
"""Get firmware version."""
return {"version": self.version}
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -1,55 +1,74 @@
"""Configure tests for the Twinkly integration."""
from collections.abc import Awaitable, Callable, Coroutine
from typing import Any
from unittest.mock import patch
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.components.twinkly import DOMAIN
from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME
from . import TEST_MODEL, TEST_NAME, TEST_UID, ClientMock
from .const import TEST_MAC, TEST_MODEL, TEST_NAME
from tests.common import MockConfigEntry
type ComponentSetup = Callable[[], Awaitable[ClientMock]]
DOMAIN = "twinkly"
TITLE = "Twinkly"
from tests.common import (
MockConfigEntry,
load_json_array_fixture,
load_json_object_fixture,
)
@pytest.fixture(name="config_entry")
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Create Twinkly entry in Home Assistant."""
client = ClientMock()
return MockConfigEntry(
domain=DOMAIN,
title=TITLE,
unique_id=TEST_UID,
entry_id=TEST_UID,
title="Twinkly",
unique_id=TEST_MAC,
data={
"host": client.host,
"id": client.id,
"name": TEST_NAME,
"model": TEST_MODEL,
"device_name": TEST_NAME,
CONF_HOST: "192.168.0.123",
CONF_ID: "497dcba3-ecbf-4587-a2dd-5eb0665e6880",
CONF_NAME: TEST_NAME,
CONF_MODEL: TEST_MODEL,
},
entry_id="01JFMME2P6RA38V5AMPCJ2JYYV",
minor_version=2,
)
@pytest.fixture(name="setup_integration")
async def mock_setup_integration(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> Callable[[], Coroutine[Any, Any, ClientMock]]:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
@pytest.fixture
def mock_twinkly_client() -> Generator[AsyncMock]:
"""Mock the Twinkly client."""
with (
patch(
"homeassistant.components.twinkly.Twinkly",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.twinkly.config_flow.Twinkly",
new=mock_client,
),
):
client = mock_client.return_value
client.get_details.return_value = load_json_object_fixture(
"get_details.json", DOMAIN
)
client.get_firmware_version.return_value = load_json_object_fixture(
"get_firmware_version.json", DOMAIN
)
client.get_saved_movies.return_value = load_json_array_fixture(
"get_saved_movies.json", DOMAIN
)
client.get_current_movie.return_value = load_json_object_fixture(
"get_current_movie.json", DOMAIN
)
client.is_on.return_value = True
client.get_brightness.return_value = {"mode": "enabled", "value": 10}
client.host = "192.168.0.123"
yield client
async def func() -> ClientMock:
mock = ClientMock()
with patch("homeassistant.components.twinkly.Twinkly", return_value=mock):
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
return mock
return func
@pytest.fixture
def mock_setup_entry() -> Generator[None]:
"""Mock setting up a config entry."""
with patch("homeassistant.components.twinkly.async_setup_entry", return_value=True):
yield

View File

@ -0,0 +1,5 @@
"""Constants for the Twinkly tests."""
TEST_MAC = "00:2d:13:3b:aa:bb"
TEST_NAME = "Tree 1"
TEST_MODEL = "TW2016"

View File

@ -0,0 +1,3 @@
{
"id": 1
}

View File

@ -0,0 +1,23 @@
{
"product_name": "Twinkly",
"product_version": "1",
"hardware_version": "1",
"flash_size": 4,
"led_type": 1,
"led_version": "1",
"product_code": "TW2016",
"device_name": "Tree 1",
"uptime": "4087441",
"rssi": -78,
"hw_id": "002d133b",
"mac": "00:2d:13:3b:aa:bb",
"uuid": "00000000-0000-0000-0000-000000000000",
"max_supported_led": 100,
"base_leds_number": 100,
"number_of_led": 100,
"led_profile": "RGB",
"frame_rate": 14,
"movie_capacity": 708,
"copyright": "LEDWORKS 2017",
"code": 1000
}

View File

@ -0,0 +1 @@
{ "version": "2.7.2" }

View File

@ -0,0 +1,4 @@
[
{ "id": 1, "name": "Rainbow" },
{ "id": 2, "name": "Flare" }
]

View File

@ -3,35 +3,64 @@
dict({
'attributes': dict({
'brightness': 26,
'color_mode': 'brightness',
'color_mode': 'rgb',
'effect': None,
'effect_list': list([
]),
'friendly_name': 'twinkly_test_device_name',
'friendly_name': 'Tree 1',
'hs_color': list([
0.0,
0.0,
]),
'rgb_color': list([
255,
255,
255,
]),
'supported_color_modes': list([
'brightness',
'rgb',
]),
'supported_features': 4,
'xy_color': list([
0.323,
0.329,
]),
}),
'device_info': dict({
'device_name': 'twinkly_test_device_name',
'base_leds_number': 100,
'code': 1000,
'copyright': 'LEDWORKS 2017',
'device_name': 'Tree 1',
'flash_size': 4,
'frame_rate': 14,
'hardware_version': '1',
'hw_id': '002d133b',
'led_profile': 'RGB',
'led_type': 1,
'led_version': '1',
'mac': '**REDACTED**',
'product_code': 'twinkly_test_device_model',
'uuid': '4c8fccf5-e08a-4173-92d5-49bf479252a2',
'max_supported_led': 100,
'movie_capacity': 708,
'number_of_led': 100,
'product_code': 'TW2016',
'product_name': 'Twinkly',
'product_version': '1',
'rssi': -78,
'uptime': '4087441',
'uuid': '00000000-0000-0000-0000-000000000000',
}),
'entry': dict({
'data': dict({
'device_name': 'twinkly_test_device_name',
'host': '**REDACTED**',
'id': '4c8fccf5-e08a-4173-92d5-49bf479252a2',
'model': 'twinkly_test_device_model',
'name': 'twinkly_test_device_name',
'id': '497dcba3-ecbf-4587-a2dd-5eb0665e6880',
'model': 'TW2016',
'name': 'Tree 1',
}),
'disabled_by': None,
'discovery_keys': dict({
}),
'domain': 'twinkly',
'entry_id': '4c8fccf5-e08a-4173-92d5-49bf479252a2',
'entry_id': '01JFMME2P6RA38V5AMPCJ2JYYV',
'minor_version': 2,
'options': dict({
}),
@ -39,9 +68,9 @@
'pref_disable_polling': False,
'source': 'user',
'title': 'Twinkly',
'unique_id': 'aa:bb:cc:dd:ee:ff',
'unique_id': '00:2d:13:3b:aa:bb',
'version': 1,
}),
'sw_version': '2.8.10',
'sw_version': '2.7.2',
})
# ---

View File

@ -0,0 +1,75 @@
# serializer version: 1
# name: test_entities[light.tree_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'effect_list': list([
]),
'supported_color_modes': list([
<ColorMode.RGB: 'rgb'>,
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.tree_1',
'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': None,
'platform': 'twinkly',
'previous_unique_id': None,
'supported_features': <LightEntityFeature: 4>,
'translation_key': 'light',
'unique_id': '00:2d:13:3b:aa:bb',
'unit_of_measurement': None,
})
# ---
# name: test_entities[light.tree_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': 26,
'color_mode': <ColorMode.RGB: 'rgb'>,
'effect': None,
'effect_list': list([
]),
'friendly_name': 'Tree 1',
'hs_color': tuple(
0.0,
0.0,
),
'rgb_color': tuple(
255,
255,
255,
),
'supported_color_modes': list([
<ColorMode.RGB: 'rgb'>,
]),
'supported_features': <LightEntityFeature: 4>,
'xy_color': tuple(
0.323,
0.329,
),
}),
'context': <ANY>,
'entity_id': 'light.tree_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -1,196 +1,170 @@
"""Tests for the config_flow of the twinly component."""
from unittest.mock import patch
from unittest.mock import AsyncMock
import pytest
from homeassistant import config_entries
from homeassistant.components import dhcp
from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.components.twinkly.const import DOMAIN
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import TEST_MODEL, TEST_NAME, ClientMock
from .const import TEST_MAC, TEST_MODEL, TEST_NAME
from tests.common import MockConfigEntry
async def test_invalid_host(hass: HomeAssistant) -> None:
"""Test the failure when invalid host provided."""
client = ClientMock()
client.is_offline = True
with patch(
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
):
result = await hass.config_entries.flow.async_init(
TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "dummy"},
)
@pytest.mark.usefixtures("mock_twinkly_client", "mock_setup_entry")
async def test_full_flow(hass: HomeAssistant) -> None:
"""Test the full flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "192.168.0.123"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: "192.168.0.123",
CONF_ID: "00000000-0000-0000-0000-000000000000",
CONF_NAME: TEST_NAME,
CONF_MODEL: TEST_MODEL,
}
assert result["result"].unique_id == TEST_MAC
@pytest.mark.usefixtures("mock_setup_entry")
async def test_exceptions(hass: HomeAssistant, mock_twinkly_client: AsyncMock) -> None:
"""Test the failure when raising exceptions."""
mock_twinkly_client.get_details.side_effect = TimeoutError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "192.168.0.123"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {CONF_HOST: "cannot_connect"}
mock_twinkly_client.get_details.side_effect = None
async def test_success_flow(hass: HomeAssistant) -> None:
"""Test that an entity is created when the flow completes."""
client = ClientMock()
with (
patch(
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
),
patch("homeassistant.components.twinkly.async_setup_entry", return_value=True),
):
result = await hass.config_entries.flow.async_init(
TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "dummy"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: "dummy",
CONF_ID: client.id,
CONF_NAME: TEST_NAME,
CONF_MODEL: TEST_MODEL,
}
async def test_dhcp_can_confirm(hass: HomeAssistant) -> None:
"""Test DHCP discovery flow can confirm right away."""
client = ClientMock()
with patch(
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
):
result = await hass.config_entries.flow.async_init(
TWINKLY_DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
hostname="Twinkly_XYZ",
ip="1.2.3.4",
macaddress="aabbccddeeff",
),
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm"
async def test_dhcp_success(hass: HomeAssistant) -> None:
"""Test DHCP discovery flow success."""
client = ClientMock()
with (
patch(
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
),
patch("homeassistant.components.twinkly.async_setup_entry", return_value=True),
):
result = await hass.config_entries.flow.async_init(
TWINKLY_DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
hostname="Twinkly_XYZ",
ip="1.2.3.4",
macaddress="aabbccddeeff",
),
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: "1.2.3.4",
CONF_ID: client.id,
CONF_NAME: TEST_NAME,
CONF_MODEL: TEST_MODEL,
}
async def test_dhcp_already_exists(hass: HomeAssistant) -> None:
"""Test DHCP discovery flow that fails to connect."""
client = ClientMock()
entry = MockConfigEntry(
domain=TWINKLY_DOMAIN,
data={
CONF_HOST: "1.2.3.4",
CONF_ID: client.id,
CONF_NAME: TEST_NAME,
CONF_MODEL: TEST_MODEL,
},
unique_id=client.id,
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "192.168.0.123"},
)
entry.add_to_hass(hass)
assert result["type"] is FlowResultType.CREATE_ENTRY
with patch(
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
):
result = await hass.config_entries.flow.async_init(
TWINKLY_DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
hostname="Twinkly_XYZ",
ip="1.2.3.4",
macaddress="aabbccddeeff",
),
)
await hass.async_block_till_done()
@pytest.mark.usefixtures("mock_twinkly_client", "mock_setup_entry")
async def test_already_configured(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test the device is already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: "192.168.0.123"}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
@pytest.mark.usefixtures("mock_twinkly_client", "mock_setup_entry")
async def test_dhcp_full_flow(hass: HomeAssistant) -> None:
"""Test DHCP discovery flow can confirm right away."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
hostname="Twinkly_XYZ",
ip="1.2.3.4",
macaddress="002d133baabb",
),
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_NAME
assert result["data"] == {
CONF_HOST: "1.2.3.4",
CONF_ID: "00000000-0000-0000-0000-000000000000",
CONF_NAME: TEST_NAME,
CONF_MODEL: TEST_MODEL,
}
assert result["result"].unique_id == TEST_MAC
@pytest.mark.usefixtures("mock_twinkly_client")
async def test_dhcp_already_configured(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test DHCP discovery flow aborts if entry already setup."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
hostname="Twinkly_XYZ",
ip="1.2.3.4",
macaddress="002d133baabb",
),
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert mock_config_entry.data[CONF_HOST] == "1.2.3.4"
@pytest.mark.usefixtures("mock_twinkly_client", "mock_setup_entry")
async def test_user_flow_works_discovery(hass: HomeAssistant) -> None:
"""Test user flow can continue after discovery happened."""
client = ClientMock()
with (
patch(
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
hostname="Twinkly_XYZ",
ip="1.2.3.4",
macaddress="002d133baabb",
),
patch("homeassistant.components.twinkly.async_setup_entry", return_value=True),
):
await hass.config_entries.flow.async_init(
TWINKLY_DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
hostname="Twinkly_XYZ",
ip="1.2.3.4",
macaddress="aabbccddeeff",
),
)
result = await hass.config_entries.flow.async_init(
TWINKLY_DOMAIN,
context={"source": SOURCE_USER},
)
assert len(hass.config_entries.flow.async_progress(TWINKLY_DOMAIN)) == 2
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "10.0.0.131"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "10.0.0.131"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
# Verify the discovery flow was aborted
assert not hass.config_entries.flow.async_progress(TWINKLY_DOMAIN)
# Verify the discovery flow was aborted
assert not hass.config_entries.flow.async_progress(DOMAIN)

View File

@ -1,32 +1,28 @@
"""Tests for the diagnostics of the twinkly component."""
from collections.abc import Awaitable, Callable
import pytest
from syrupy import SnapshotAssertion
from syrupy.filters import props
from homeassistant.core import HomeAssistant
from . import ClientMock
from . import setup_integration
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
type ComponentSetup = Callable[[], Awaitable[ClientMock]]
DOMAIN = "twinkly"
@pytest.mark.usefixtures("mock_twinkly_client")
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
setup_integration: ComponentSetup,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test diagnostics."""
await setup_integration()
entry = hass.config_entries.async_entries(DOMAIN)[0]
await setup_integration(hass, mock_config_entry)
assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot(
exclude=props("created_at", "modified_at")
)
assert await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
) == snapshot(exclude=props("created_at", "modified_at"))

View File

@ -1,7 +1,9 @@
"""Tests of the initialization of the twinkly integration."""
from unittest.mock import patch
from uuid import uuid4
from unittest.mock import AsyncMock
from aiohttp import ClientConnectionError
import pytest
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.twinkly.const import DOMAIN
@ -10,82 +12,55 @@ from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import TEST_HOST, TEST_MAC, TEST_MODEL, TEST_NAME_ORIGINAL, ClientMock
from . import setup_integration
from .const import TEST_MAC, TEST_MODEL
from tests.common import MockConfigEntry
async def test_load_unload_entry(hass: HomeAssistant) -> None:
"""Validate that setup entry also configure the client."""
client = ClientMock()
@pytest.mark.usefixtures("mock_twinkly_client")
async def test_load_unload_entry(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test the load/unload of the config entry."""
device_id = str(uuid4())
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: TEST_HOST,
CONF_ID: device_id,
CONF_NAME: TEST_NAME_ORIGINAL,
CONF_MODEL: TEST_MODEL,
},
entry_id=device_id,
unique_id=TEST_MAC,
minor_version=2,
)
await setup_integration(hass, mock_config_entry)
config_entry.add_to_hass(hass)
assert mock_config_entry.state is ConfigEntryState.LOADED
with patch("homeassistant.components.twinkly.Twinkly", return_value=client):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.config_entries.async_unload(mock_config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_config_entry_not_ready(hass: HomeAssistant) -> None:
async def test_config_entry_not_ready(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Validate that config entry is retried."""
client = ClientMock()
client.is_offline = True
mock_twinkly_client.get_details.side_effect = ClientConnectionError
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: TEST_HOST,
CONF_ID: id,
CONF_NAME: TEST_NAME_ORIGINAL,
CONF_MODEL: TEST_MODEL,
},
minor_version=2,
unique_id=TEST_MAC,
)
await setup_integration(hass, mock_config_entry)
config_entry.add_to_hass(hass)
with patch("homeassistant.components.twinkly.Twinkly", return_value=client):
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.usefixtures("mock_twinkly_client")
async def test_mac_migration(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Validate that the unique_id is migrated to the MAC address."""
client = ClientMock()
config_entry = MockConfigEntry(
domain=DOMAIN,
minor_version=1,
unique_id="unique_id",
data={
CONF_HOST: TEST_HOST,
CONF_HOST: "192.168.0.123",
CONF_ID: id,
CONF_NAME: TEST_NAME_ORIGINAL,
CONF_NAME: "Tree 1",
CONF_MODEL: TEST_MODEL,
},
)
@ -100,8 +75,7 @@ async def test_mac_migration(
identifiers={(DOMAIN, config_entry.unique_id)},
)
with patch("homeassistant.components.twinkly.Twinkly", return_value=client):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED

View File

@ -3,290 +3,287 @@
from __future__ import annotations
from datetime import timedelta
from unittest.mock import patch
from typing import Any
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.light import ATTR_BRIGHTNESS, LightEntityFeature
from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN
from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
DOMAIN as LIGHT_DOMAIN,
LightEntityFeature,
)
from homeassistant.components.twinkly import DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.entity_registry import RegistryEntry
from . import TEST_MAC, TEST_MODEL, TEST_NAME, TEST_NAME_ORIGINAL, ClientMock
from . import setup_integration
from .const import TEST_MAC
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
async def test_initial_state(hass: HomeAssistant) -> None:
"""Validate that entity and device states are updated on startup."""
entity, device, _, _ = await _create_entries(hass)
@pytest.mark.usefixtures("mock_twinkly_client")
async def test_entities(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the created entities."""
with patch("homeassistant.components.twinkly.PLATFORMS", [Platform.LIGHT]):
await setup_integration(hass, mock_config_entry)
state = hass.states.get(entity.entity_id)
# Basic state properties
assert state.name == TEST_NAME
assert state.state == "on"
assert state.attributes[ATTR_BRIGHTNESS] == 26
assert state.attributes["friendly_name"] == TEST_NAME
assert device.name == TEST_NAME
assert device.model == TEST_MODEL
assert device.manufacturer == "LEDWORKS"
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_turn_on_off(hass: HomeAssistant) -> None:
async def test_turn_on_off(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Test support of the light.turn_on service."""
client = ClientMock()
client.state = False
client.brightness = {"mode": "enabled", "value": 20}
entity, _, _, _ = await _create_entries(hass, client)
mock_twinkly_client.is_on.return_value = False
assert hass.states.get(entity.entity_id).state == "off"
await setup_integration(hass, mock_config_entry)
assert hass.states.get("light.tree_1").state == STATE_OFF
await hass.services.async_call(
"light", "turn_on", service_data={"entity_id": entity.entity_id}, blocking=True
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "light.tree_1"},
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "on"
assert state.attributes[ATTR_BRIGHTNESS] == 51
mock_twinkly_client.turn_on.assert_called_once_with()
async def test_turn_on_with_brightness(hass: HomeAssistant) -> None:
async def test_turn_on_with_brightness(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Test support of the light.turn_on service with a brightness parameter."""
client = ClientMock()
client.state = False
client.brightness = {"mode": "enabled", "value": 20}
entity, _, _, _ = await _create_entries(hass, client)
mock_twinkly_client.is_on.return_value = False
assert hass.states.get(entity.entity_id).state == "off"
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
"light",
"turn_on",
service_data={"entity_id": entity.entity_id, "brightness": 255},
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_BRIGHTNESS: 255},
blocking=True,
)
state = hass.states.get(entity.entity_id)
mock_twinkly_client.set_brightness.assert_called_once_with(100)
mock_twinkly_client.turn_on.assert_called_once_with()
assert state.state == "on"
assert state.attributes[ATTR_BRIGHTNESS] == 255
async def test_brightness_to_zero(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Test support of the light.turn_on service with a brightness parameter."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
"light",
"turn_on",
service_data={"entity_id": entity.entity_id, "brightness": 1},
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_BRIGHTNESS: 1},
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "off"
mock_twinkly_client.set_brightness.assert_not_called()
mock_twinkly_client.turn_off.assert_called_once_with()
async def test_turn_on_with_color_rgbw(hass: HomeAssistant) -> None:
async def test_turn_on_with_color_rgbw(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Test support of the light.turn_on service with a rgbw parameter."""
client = ClientMock()
client.state = False
client.device_info["led_profile"] = "RGBW"
client.brightness = {"mode": "enabled", "value": 255}
entity, _, _, _ = await _create_entries(hass, client)
mock_twinkly_client.is_on.return_value = False
mock_twinkly_client.get_details.return_value["led_profile"] = "RGBW"
assert hass.states.get(entity.entity_id).state == "off"
await setup_integration(hass, mock_config_entry)
assert (
LightEntityFeature.EFFECT
& hass.states.get(entity.entity_id).attributes["supported_features"]
& hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES]
)
await hass.services.async_call(
"light",
"turn_on",
service_data={"entity_id": entity.entity_id, "rgbw_color": (128, 64, 32, 0)},
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data={
ATTR_ENTITY_ID: "light.tree_1",
ATTR_RGBW_COLOR: (128, 64, 32, 0),
},
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "on"
assert client.color == (128, 64, 32)
assert client.default_mode == "color"
assert client.mode == "color"
mock_twinkly_client.interview.assert_called_once_with()
mock_twinkly_client.set_static_colour.assert_called_once_with((128, 64, 32))
mock_twinkly_client.set_mode.assert_called_once_with("color")
assert mock_twinkly_client.default_mode == "color"
async def test_turn_on_with_color_rgb(hass: HomeAssistant) -> None:
async def test_turn_on_with_color_rgb(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Test support of the light.turn_on service with a rgb parameter."""
client = ClientMock()
client.state = False
client.device_info["led_profile"] = "RGB"
client.brightness = {"mode": "enabled", "value": 255}
entity, _, _, _ = await _create_entries(hass, client)
mock_twinkly_client.is_on.return_value = False
mock_twinkly_client.get_details.return_value["led_profile"] = "RGB"
assert hass.states.get(entity.entity_id).state == "off"
await setup_integration(hass, mock_config_entry)
assert (
LightEntityFeature.EFFECT
& hass.states.get(entity.entity_id).attributes["supported_features"]
& hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES]
)
await hass.services.async_call(
"light",
"turn_on",
service_data={"entity_id": entity.entity_id, "rgb_color": (128, 64, 32)},
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_RGB_COLOR: (128, 64, 32)},
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "on"
assert client.color == (128, 64, 32)
assert client.default_mode == "color"
assert client.mode == "color"
mock_twinkly_client.interview.assert_called_once_with()
mock_twinkly_client.set_static_colour.assert_called_once_with((128, 64, 32))
mock_twinkly_client.set_mode.assert_called_once_with("color")
assert mock_twinkly_client.default_mode == "color"
async def test_turn_on_with_effect(hass: HomeAssistant) -> None:
async def test_turn_on_with_effect(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Test support of the light.turn_on service with effects."""
client = ClientMock()
client.state = False
client.device_info["led_profile"] = "RGB"
client.brightness = {"mode": "enabled", "value": 255}
entity, _, _, _ = await _create_entries(hass, client)
mock_twinkly_client.is_on.return_value = False
mock_twinkly_client.get_details.return_value["led_profile"] = "RGB"
assert hass.states.get(entity.entity_id).state == "off"
assert not client.current_movie
await setup_integration(hass, mock_config_entry)
assert (
LightEntityFeature.EFFECT
& hass.states.get(entity.entity_id).attributes["supported_features"]
& hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES]
)
await hass.services.async_call(
"light",
"turn_on",
service_data={"entity_id": entity.entity_id, "effect": "1 Rainbow"},
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_EFFECT: "2 Rainbow"},
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "on"
assert client.current_movie["id"] == 1
assert client.default_mode == "movie"
assert client.mode == "movie"
mock_twinkly_client.interview.assert_called_once_with()
mock_twinkly_client.set_current_movie.assert_called_once_with(2)
mock_twinkly_client.set_mode.assert_called_once_with("movie")
assert mock_twinkly_client.default_mode == "movie"
async def test_turn_on_with_color_rgbw_and_missing_effect(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
("data"),
[
{ATTR_RGBW_COLOR: (128, 64, 32, 0)},
{ATTR_RGB_COLOR: (128, 64, 32)},
],
)
async def test_turn_on_with_missing_effect(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
data: dict[str, Any],
) -> None:
"""Test support of the light.turn_on service with rgbw color and missing effect support."""
client = ClientMock()
client.state = False
client.device_info["led_profile"] = "RGBW"
client.brightness = {"mode": "enabled", "value": 255}
client.version = "2.7.0"
entity, _, _, _ = await _create_entries(hass, client)
mock_twinkly_client.is_on.return_value = False
mock_twinkly_client.get_firmware_version.return_value["version"] = "2.7.0"
assert hass.states.get(entity.entity_id).state == "off"
await setup_integration(hass, mock_config_entry)
assert (
not LightEntityFeature.EFFECT
& hass.states.get(entity.entity_id).attributes["supported_features"]
LightEntityFeature.EFFECT
^ hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES]
)
await hass.services.async_call(
"light",
"turn_on",
service_data={"entity_id": entity.entity_id, "rgbw_color": (128, 64, 32, 0)},
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "light.tree_1"} | data,
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "on"
assert client.color == (0, 128, 64, 32)
assert client.mode == "movie"
assert client.default_mode == "movie"
mock_twinkly_client.interview.assert_called_once_with()
mock_twinkly_client.set_cycle_colours.assert_called_once_with((128, 64, 32))
mock_twinkly_client.set_mode.assert_called_once_with("movie")
assert mock_twinkly_client.default_mode == "movie"
mock_twinkly_client.set_current_movie.assert_not_called()
async def test_turn_on_with_color_rgb_and_missing_effect(hass: HomeAssistant) -> None:
"""Test support of the light.turn_on service with rgb color and missing effect support."""
client = ClientMock()
client.state = False
client.device_info["led_profile"] = "RGB"
client.brightness = {"mode": "enabled", "value": 255}
client.version = "2.7.0"
entity, _, _, _ = await _create_entries(hass, client)
async def test_turn_on_with_color_rgbw_and_missing_effect(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Test support of the light.turn_on service with missing effect support."""
mock_twinkly_client.is_on.return_value = False
mock_twinkly_client.get_firmware_version.return_value["version"] = "2.7.0"
assert hass.states.get(entity.entity_id).state == "off"
await setup_integration(hass, mock_config_entry)
assert (
not LightEntityFeature.EFFECT
& hass.states.get(entity.entity_id).attributes["supported_features"]
LightEntityFeature.EFFECT
^ hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES]
)
await hass.services.async_call(
"light",
"turn_on",
service_data={"entity_id": entity.entity_id, "rgb_color": (128, 64, 32)},
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_EFFECT: "2 Rainbow"},
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "on"
assert client.color == (128, 64, 32)
assert client.mode == "movie"
assert client.default_mode == "movie"
mock_twinkly_client.set_current_movie.assert_not_called()
async def test_turn_on_with_effect_missing_effects(hass: HomeAssistant) -> None:
"""Test support of the light.turn_on service with effect set even if effects are not supported."""
client = ClientMock()
client.state = False
client.device_info["led_profile"] = "RGB"
client.brightness = {"mode": "enabled", "value": 255}
client.version = "2.7.0"
entity, _, _, _ = await _create_entries(hass, client)
assert hass.states.get(entity.entity_id).state == "off"
assert not client.current_movie
assert (
not LightEntityFeature.EFFECT
& hass.states.get(entity.entity_id).attributes["supported_features"]
)
await hass.services.async_call(
"light",
"turn_on",
service_data={"entity_id": entity.entity_id, "effect": "1 Rainbow"},
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "on"
assert not client.current_movie
assert client.default_mode == "movie"
assert client.mode == "movie"
async def test_turn_off(hass: HomeAssistant) -> None:
async def test_turn_off(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Test support of the light.turn_off service."""
entity, _, _, _ = await _create_entries(hass)
assert hass.states.get(entity.entity_id).state == "on"
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
"light", "turn_off", service_data={"entity_id": entity.entity_id}, blocking=True
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
service_data={ATTR_ENTITY_ID: "light.tree_1"},
blocking=True,
)
state = hass.states.get(entity.entity_id)
assert state.state == "off"
mock_twinkly_client.turn_off.assert_called_once_with()
async def test_update_name(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
freezer: FrozenDateTimeFactory,
mock_config_entry: MockConfigEntry,
mock_twinkly_client: AsyncMock,
) -> None:
"""Validate device's name update behavior.
@ -294,56 +291,15 @@ async def test_update_name(
then the name of the entity is updated and it's also persisted,
so it can be restored when starting HA while Twinkly is offline.
"""
entity, _, client, config_entry = await _create_entries(hass)
client.change_name("new_device_name")
await setup_integration(hass, mock_config_entry)
mock_twinkly_client.get_details.return_value["device_name"] = "new_device_name"
freezer.tick(timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
dev_entry = device_registry.async_get_device({(TWINKLY_DOMAIN, TEST_MAC)})
dev_entry = device_registry.async_get_device({(DOMAIN, TEST_MAC)})
assert dev_entry.name == "new_device_name"
assert config_entry.data[CONF_NAME] == "new_device_name"
async def test_unload(hass: HomeAssistant) -> None:
"""Validate that entities can be unloaded from the UI."""
_, _, _, entry = await _create_entries(hass)
assert await hass.config_entries.async_unload(entry.entry_id)
async def _create_entries(
hass: HomeAssistant, client=None
) -> tuple[RegistryEntry, DeviceEntry, ClientMock]:
client = ClientMock() if client is None else client
with patch("homeassistant.components.twinkly.Twinkly", return_value=client):
config_entry = MockConfigEntry(
domain=TWINKLY_DOMAIN,
data={
CONF_HOST: client,
CONF_ID: client.id,
CONF_NAME: TEST_NAME_ORIGINAL,
CONF_MODEL: TEST_MODEL,
},
unique_id=TEST_MAC,
minor_version=2,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, TEST_MAC)
entity_entry = entity_registry.async_get(entity_id)
device = device_registry.async_get_device(identifiers={(TWINKLY_DOMAIN, TEST_MAC)})
assert entity_entry is not None
assert device is not None
return entity_entry, device, client, config_entry