Add light support to WMS WebControl pro (#128308)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Marc Hörsken 2024-10-25 18:20:40 +02:00 committed by GitHub
parent a948c7d69d
commit 0e789be09f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 391 additions and 1 deletions

View file

@ -15,7 +15,7 @@ from homeassistant.helpers.typing import UNDEFINED
from .const import DOMAIN, MANUFACTURER
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SCENE]
PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT, Platform.SCENE]
type WebControlProConfigEntry = ConfigEntry[WebControlPro]

View file

@ -5,3 +5,5 @@ SUGGESTED_HOST = "webcontrol"
ATTRIBUTION = "Data provided by WMS WebControl pro API"
MANUFACTURER = "WAREMA Renkhoff SE"
BRIGHTNESS_SCALE = (1, 100)

View file

@ -0,0 +1,89 @@
"""Support for lights connected with WMS WebControl pro."""
from __future__ import annotations
from datetime import timedelta
from typing import Any
from wmspro.const import WMS_WebControl_pro_API_actionDescription
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.color import brightness_to_value, value_to_brightness
from . import WebControlProConfigEntry
from .const import BRIGHTNESS_SCALE
from .entity import WebControlProGenericEntity
SCAN_INTERVAL = timedelta(seconds=5)
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: WebControlProConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the WMS based lights from a config entry."""
hub = config_entry.runtime_data
entities: list[WebControlProGenericEntity] = []
for dest in hub.dests.values():
if dest.action(WMS_WebControl_pro_API_actionDescription.LightDimming):
entities.append(WebControlProDimmer(config_entry.entry_id, dest))
elif dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch):
entities.append(WebControlProLight(config_entry.entry_id, dest))
async_add_entities(entities)
class WebControlProLight(WebControlProGenericEntity, LightEntity):
"""Representation of a WMS based light."""
_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
@property
def is_on(self) -> bool:
"""Return true if light is on."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch)
return action["onOffState"]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch)
await action(onOffState=True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch)
await action(onOffState=False)
class WebControlProDimmer(WebControlProLight):
"""Representation of a WMS-based dimmable light."""
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
@property
def brightness(self) -> int:
"""Return the brightness of this light between 1..255."""
action = self._dest.action(
WMS_WebControl_pro_API_actionDescription.LightDimming
)
return value_to_brightness(BRIGHTNESS_SCALE, action["percentage"])
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the dimmer on."""
if ATTR_BRIGHTNESS not in kwargs:
await super().async_turn_on(**kwargs)
return
action = self._dest.action(
WMS_WebControl_pro_API_actionDescription.LightDimming
)
await action(
percentage=brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])
)

View file

@ -82,6 +82,18 @@ def mock_hub_status_prod_awning() -> Generator[AsyncMock]:
yield mock_dest_refresh
@pytest.fixture
def mock_hub_status_prod_dimmer() -> Generator[AsyncMock]:
"""Override WebControlPro._getStatus."""
with patch(
"wmspro.webcontrol.WebControlPro._getStatus",
return_value=load_json_object_fixture(
"example_status_prod_dimmer.json", DOMAIN
),
) as mock_dest_refresh:
yield mock_dest_refresh
@pytest.fixture
def mock_dest_refresh() -> Generator[AsyncMock]:
"""Override Destination.refresh."""

View file

@ -0,0 +1,28 @@
{
"command": "getStatus",
"protocolVersion": "1.0.0",
"details": [
{
"destinationId": 97358,
"data": {
"drivingCause": 0,
"heartbeatError": false,
"blocking": false,
"productData": [
{
"actionId": 0,
"value": {
"percentage": 0
}
},
{
"actionId": 20,
"value": {
"onOffState": false
}
}
]
}
}
]
}

View file

@ -0,0 +1,53 @@
# serializer version: 1
# name: test_light_device
DeviceRegistryEntrySnapshot({
'area_id': 'terrasse',
'config_entries': <ANY>,
'configuration_url': 'http://webcontrol/control',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'wmspro',
'97358',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'WAREMA Renkhoff SE',
'model': 'Dimmer',
'model_id': None,
'name': 'Licht',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '97358',
'suggested_area': 'Terrasse',
'sw_version': None,
'via_device_id': <ANY>,
})
# ---
# name: test_light_update
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by WMS WebControl pro API',
'brightness': None,
'color_mode': None,
'friendly_name': 'Licht',
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.licht',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View file

@ -0,0 +1,206 @@
"""Test the wmspro light support."""
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
from syrupy import SnapshotAssertion
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.wmspro.const import DOMAIN
from homeassistant.components.wmspro.light import SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_config_entry
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_light_device(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hub_ping: AsyncMock,
mock_hub_configuration_prod: AsyncMock,
mock_hub_status_prod_dimmer: AsyncMock,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test that a light device is created correctly."""
assert await setup_config_entry(hass, mock_config_entry)
assert len(mock_hub_ping.mock_calls) == 1
assert len(mock_hub_configuration_prod.mock_calls) == 1
assert len(mock_hub_status_prod_dimmer.mock_calls) == 2
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "97358")})
assert device_entry is not None
assert device_entry == snapshot
async def test_light_update(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hub_ping: AsyncMock,
mock_hub_configuration_prod: AsyncMock,
mock_hub_status_prod_dimmer: AsyncMock,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
) -> None:
"""Test that a light entity is created and updated correctly."""
assert await setup_config_entry(hass, mock_config_entry)
assert len(mock_hub_ping.mock_calls) == 1
assert len(mock_hub_configuration_prod.mock_calls) == 1
assert len(mock_hub_status_prod_dimmer.mock_calls) == 2
entity = hass.states.get("light.licht")
assert entity is not None
assert entity == snapshot
# Move time to next update
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert len(mock_hub_status_prod_dimmer.mock_calls) >= 3
async def test_light_turn_on_and_off(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hub_ping: AsyncMock,
mock_hub_configuration_prod: AsyncMock,
mock_hub_status_prod_dimmer: AsyncMock,
mock_action_call: AsyncMock,
) -> None:
"""Test that a light entity is turned on and off correctly."""
assert await setup_config_entry(hass, mock_config_entry)
assert len(mock_hub_ping.mock_calls) == 1
assert len(mock_hub_configuration_prod.mock_calls) == 1
assert len(mock_hub_status_prod_dimmer.mock_calls) >= 1
entity = hass.states.get("light.licht")
assert entity is not None
assert entity.state == STATE_OFF
assert entity.attributes[ATTR_BRIGHTNESS] is None
with patch(
"wmspro.destination.Destination.refresh",
return_value=True,
):
before = len(mock_hub_status_prod_dimmer.mock_calls)
await hass.services.async_call(
Platform.LIGHT,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=True,
)
entity = hass.states.get("light.licht")
assert entity is not None
assert entity.state == STATE_ON
assert entity.attributes[ATTR_BRIGHTNESS] >= 1
assert len(mock_hub_status_prod_dimmer.mock_calls) == before
with patch(
"wmspro.destination.Destination.refresh",
return_value=True,
):
before = len(mock_hub_status_prod_dimmer.mock_calls)
await hass.services.async_call(
Platform.LIGHT,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=True,
)
entity = hass.states.get("light.licht")
assert entity is not None
assert entity.state == STATE_OFF
assert entity.attributes[ATTR_BRIGHTNESS] is None
assert len(mock_hub_status_prod_dimmer.mock_calls) == before
async def test_light_dimm_on_and_off(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hub_ping: AsyncMock,
mock_hub_configuration_prod: AsyncMock,
mock_hub_status_prod_dimmer: AsyncMock,
mock_action_call: AsyncMock,
) -> None:
"""Test that a light entity is dimmed on and off correctly."""
assert await setup_config_entry(hass, mock_config_entry)
assert len(mock_hub_ping.mock_calls) == 1
assert len(mock_hub_configuration_prod.mock_calls) == 1
assert len(mock_hub_status_prod_dimmer.mock_calls) >= 1
entity = hass.states.get("light.licht")
assert entity is not None
assert entity.state == STATE_OFF
assert entity.attributes[ATTR_BRIGHTNESS] is None
with patch(
"wmspro.destination.Destination.refresh",
return_value=True,
):
before = len(mock_hub_status_prod_dimmer.mock_calls)
await hass.services.async_call(
Platform.LIGHT,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=True,
)
entity = hass.states.get("light.licht")
assert entity is not None
assert entity.state == STATE_ON
assert entity.attributes[ATTR_BRIGHTNESS] >= 1
assert len(mock_hub_status_prod_dimmer.mock_calls) == before
with patch(
"wmspro.destination.Destination.refresh",
return_value=True,
):
before = len(mock_hub_status_prod_dimmer.mock_calls)
await hass.services.async_call(
Platform.LIGHT,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, ATTR_BRIGHTNESS: 128},
blocking=True,
)
entity = hass.states.get("light.licht")
assert entity is not None
assert entity.state == STATE_ON
assert entity.attributes[ATTR_BRIGHTNESS] == 128
assert len(mock_hub_status_prod_dimmer.mock_calls) == before
with patch(
"wmspro.destination.Destination.refresh",
return_value=True,
):
before = len(mock_hub_status_prod_dimmer.mock_calls)
await hass.services.async_call(
Platform.LIGHT,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=True,
)
entity = hass.states.get("light.licht")
assert entity is not None
assert entity.state == STATE_OFF
assert entity.attributes[ATTR_BRIGHTNESS] is None
assert len(mock_hub_status_prod_dimmer.mock_calls) == before