mirror of
https://github.com/home-assistant/core.git
synced 2026-07-04 13:15:29 +01:00
Support WiZ lights with unadvertised dual head ratio (#172854)
This commit is contained in:
@@ -26,6 +26,9 @@ class WizNumberEntityDescription(NumberEntityDescription):
|
||||
required_feature: str
|
||||
set_value_fn: Callable[[wizlight, int], Coroutine[None, None, None]]
|
||||
value_fn: Callable[[wizlight], int | None]
|
||||
# Optional fallback, checked against runtime state when required_feature is
|
||||
# not advertised by the bulb type.
|
||||
supported_fn: Callable[[wizlight], bool] | None = None
|
||||
|
||||
|
||||
async def _async_set_speed(device: wizlight, speed: int) -> None:
|
||||
@@ -57,11 +60,30 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = (
|
||||
value_fn=lambda device: cast(int | None, device.state.get_ratio()),
|
||||
set_value_fn=_async_set_ratio,
|
||||
required_feature="dual_head",
|
||||
# Some ratio-based dual-head lights do not advertise this feature.
|
||||
supported_fn=lambda device: (
|
||||
device.state is not None and device.state.get_ratio() is not None
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _supports_number_description(
|
||||
device: wizlight, description: WizNumberEntityDescription
|
||||
) -> bool:
|
||||
"""Return whether the device supports a number description.
|
||||
|
||||
When the bulb type does not advertise the required feature, ``supported_fn``
|
||||
is evaluated as a fallback. It inspects the current runtime state (e.g.
|
||||
whether a ratio is present), so the result depends on the device state at
|
||||
call time.
|
||||
"""
|
||||
return getattr(device.bulbtype.features, description.required_feature, False) or (
|
||||
description.supported_fn is not None and description.supported_fn(device)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: WizConfigEntry,
|
||||
@@ -71,9 +93,7 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
WizSpeedNumber(entry.runtime_data, entry.title, description)
|
||||
for description in NUMBERS
|
||||
if getattr(
|
||||
entry.runtime_data.bulb.bulbtype.features, description.required_feature
|
||||
)
|
||||
if _supports_number_description(entry.runtime_data.bulb, description)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Tests for the number platform."""
|
||||
|
||||
from pywizlight import PilotParser
|
||||
|
||||
from homeassistant.components.number import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.components.wiz.const import DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -12,11 +15,27 @@ from homeassistant.helpers import entity_registry as er
|
||||
from . import (
|
||||
FAKE_DUAL_HEAD_RGBWW_BULB,
|
||||
FAKE_MAC,
|
||||
FAKE_TURNABLE_BULB,
|
||||
_mocked_wizlight,
|
||||
async_push_update,
|
||||
async_setup_integration,
|
||||
)
|
||||
|
||||
|
||||
async def test_ratio_not_created_without_ratio(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test the ratio entity is not created for lights that report no ratio."""
|
||||
await async_setup_integration(hass, bulb_type=FAKE_TURNABLE_BULB)
|
||||
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
NUMBER_DOMAIN, DOMAIN, f"{FAKE_MAC}_dual_head_ratio"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_speed_operation(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
@@ -47,10 +66,10 @@ async def test_ratio_operation(
|
||||
"""Test changing a dual head ratio."""
|
||||
bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_DUAL_HEAD_RGBWW_BULB)
|
||||
await async_push_update(hass, bulb, {"mac": FAKE_MAC})
|
||||
entity_id = "number.mock_title_dual_head_ratio"
|
||||
assert (
|
||||
entity_registry.async_get(entity_id).unique_id == f"{FAKE_MAC}_dual_head_ratio"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
NUMBER_DOMAIN, DOMAIN, f"{FAKE_MAC}_dual_head_ratio"
|
||||
)
|
||||
assert entity_id is not None
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
await async_push_update(hass, bulb, {"mac": FAKE_MAC, "ratio": 50})
|
||||
@@ -65,3 +84,36 @@ async def test_ratio_operation(
|
||||
bulb.set_ratio.assert_called_with(30)
|
||||
await async_push_update(hass, bulb, {"mac": FAKE_MAC, "ratio": 30})
|
||||
assert hass.states.get(entity_id).state == "30.0"
|
||||
|
||||
|
||||
async def test_ratio_operation_without_dual_head_feature(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test changing a ratio reported by a light with an unadvertised dual head feature."""
|
||||
bulb = _mocked_wizlight(None, None, FAKE_TURNABLE_BULB)
|
||||
bulb.state = None
|
||||
|
||||
async def _update_state() -> PilotParser:
|
||||
bulb.state = PilotParser({"mac": FAKE_MAC, "ratio": 50})
|
||||
return bulb.state
|
||||
|
||||
bulb.updateState.side_effect = _update_state
|
||||
|
||||
await async_setup_integration(hass, wizlight=bulb)
|
||||
|
||||
bulb.updateState.assert_called_once()
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
NUMBER_DOMAIN, DOMAIN, f"{FAKE_MAC}_dual_head_ratio"
|
||||
)
|
||||
assert entity_id is not None
|
||||
assert hass.states.get(entity_id).state == "50.0"
|
||||
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 30},
|
||||
blocking=True,
|
||||
)
|
||||
bulb.set_ratio.assert_called_with(30)
|
||||
await async_push_update(hass, bulb, {"mac": FAKE_MAC, "ratio": 30})
|
||||
assert hass.states.get(entity_id).state == "30.0"
|
||||
|
||||
Reference in New Issue
Block a user