1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-27 14:31:13 +00:00

Add support for load switches to WMS WebControl pro (#151047)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Marc Hörsken
2025-12-22 15:12:28 +01:00
committed by GitHub
parent aae98a77d5
commit ca30d8b1c2
7 changed files with 533 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ PLATFORMS: list[Platform] = [
Platform.COVER,
Platform.LIGHT,
Platform.SCENE,
Platform.SWITCH,
]
type WebControlProConfigEntry = ConfigEntry[WebControlPro]

View File

@@ -0,0 +1,62 @@
"""Support for loads 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,
WMS_WebControl_pro_API_responseType,
)
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import WebControlProConfigEntry
from .entity import WebControlProGenericEntity
SCAN_INTERVAL = timedelta(seconds=15)
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: WebControlProConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the WMS based switches from a config entry."""
hub = config_entry.runtime_data
async_add_entities(
WebControlProSwitch(config_entry.entry_id, dest)
for dest in hub.dests.values()
if dest.hasAction(WMS_WebControl_pro_API_actionDescription.LoadSwitch)
)
class WebControlProSwitch(WebControlProGenericEntity, SwitchEntity):
"""Representation of a WMS based switch."""
_attr_name = None
@property
def is_on(self) -> bool:
"""Return true if switch is on."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LoadSwitch)
return action["onOffState"]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LoadSwitch)
await action(
onOffState=True, responseType=WMS_WebControl_pro_API_responseType.Detailed
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LoadSwitch)
await action(
onOffState=False, responseType=WMS_WebControl_pro_API_responseType.Detailed
)

View File

@@ -82,6 +82,16 @@ def mock_hub_configuration_prod_awning_valance() -> Generator[AsyncMock]:
yield mock_hub_configuration
@pytest.fixture
def mock_hub_configuration_prod_load_switch() -> Generator[AsyncMock]:
"""Override WebControlPro._getConfiguration."""
with patch(
"wmspro.webcontrol.WebControlPro._getConfiguration",
return_value=load_json_object_fixture("config_prod_load_switch.json", DOMAIN),
) as mock_hub_configuration:
yield mock_hub_configuration
@pytest.fixture
def mock_hub_configuration_prod_roller_shutter() -> Generator[AsyncMock]:
"""Override WebControlPro._getConfiguration."""
@@ -114,6 +124,16 @@ def mock_hub_status_prod_dimmer() -> Generator[AsyncMock]:
yield mock_hub_status
@pytest.fixture
def mock_hub_status_prod_load_switch() -> Generator[AsyncMock]:
"""Override WebControlPro._getStatus."""
with patch(
"wmspro.webcontrol.WebControlPro._getStatus",
return_value=load_json_object_fixture("status_prod_load_switch.json", DOMAIN),
) as mock_hub_status:
yield mock_hub_status
@pytest.fixture
def mock_hub_status_prod_roller_shutter() -> Generator[AsyncMock]:
"""Override WebControlPro._getStatus."""

View File

@@ -0,0 +1,249 @@
{
"command": "getConfiguration",
"protocolVersion": "1.0.0",
"destinations": [
{
"id": 65355,
"animationType": 3,
"names": ["DACH", "", "", ""],
"actions": [
{
"id": 6,
"actionType": 2,
"actionDescription": 3,
"minValue": -127,
"maxValue": 127
},
{
"id": 16,
"actionType": 6,
"actionDescription": 12
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
},
{
"id": 23,
"actionType": 7,
"actionDescription": 12
}
]
},
{
"id": 90732,
"animationType": 1,
"names": ["MARKISE LINKS", "", "", ""],
"actions": [
{
"id": 0,
"actionType": 0,
"actionDescription": 0,
"minValue": 0,
"maxValue": 100
},
{
"id": 16,
"actionType": 6,
"actionDescription": 12
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
}
]
},
{
"id": 159890,
"animationType": 1,
"names": ["MARKISE VORNE", "", "", ""],
"actions": [
{
"id": 0,
"actionType": 0,
"actionDescription": 0,
"minValue": 0,
"maxValue": 100
},
{
"id": 16,
"actionType": 6,
"actionDescription": 12
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
}
]
},
{
"id": 201106,
"animationType": 1,
"names": ["MARKISE RECHTS", "", "", ""],
"actions": [
{
"id": 0,
"actionType": 0,
"actionDescription": 0,
"minValue": 0,
"maxValue": 100
},
{
"id": 16,
"actionType": 6,
"actionDescription": 12
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
}
]
},
{
"id": 317448,
"animationType": 6,
"names": ["LICHT SENKRECHT", "", "", ""],
"actions": [
{
"id": 0,
"actionType": 0,
"actionDescription": 8,
"minValue": 0,
"maxValue": 100
},
{
"id": 17,
"actionType": 6,
"actionDescription": 12
},
{
"id": 20,
"actionType": 4,
"actionDescription": 6
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
}
]
},
{
"id": 382168,
"animationType": 6,
"names": ["LICHT OBEN", "", "", ""],
"actions": [
{
"id": 0,
"actionType": 0,
"actionDescription": 8,
"minValue": 0,
"maxValue": 100
},
{
"id": 17,
"actionType": 6,
"actionDescription": 12
},
{
"id": 20,
"actionType": 4,
"actionDescription": 6
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
}
]
},
{
"id": 414040,
"animationType": 6,
"names": ["LICHT UNTEN", "", "", ""],
"actions": [
{
"id": 0,
"actionType": 0,
"actionDescription": 8,
"minValue": 0,
"maxValue": 100
},
{
"id": 17,
"actionType": 6,
"actionDescription": 12
},
{
"id": 20,
"actionType": 4,
"actionDescription": 6
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
}
]
},
{
"id": 499120,
"animationType": 5,
"names": ["HEIZUNG LINKS", "", "", ""],
"actions": [
{
"id": 20,
"actionType": 4,
"actionDescription": 7
},
{
"id": 21,
"actionType": 5,
"actionDescription": 10
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
}
]
},
{
"id": 533844,
"animationType": 5,
"names": ["HEIZUNG RECHTS", "", "", ""],
"actions": [
{
"id": 20,
"actionType": 4,
"actionDescription": 7
},
{
"id": 21,
"actionType": 5,
"actionDescription": 10
},
{
"id": 22,
"actionType": 8,
"actionDescription": 13
}
]
}
],
"rooms": [
{
"id": 39443,
"name": "Terasse",
"destinations": [
65355, 90732, 159890, 201106, 317448, 382168, 414040, 499120, 533844
],
"scenes": []
}
],
"scenes": []
}

View File

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

View File

@@ -0,0 +1,46 @@
# serializer version: 1
# name: test_switch_device
DeviceRegistryEntrySnapshot({
'area_id': 'terasse',
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'http://webcontrol/control',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'wmspro',
'499120',
),
}),
'labels': set({
}),
'manufacturer': 'WAREMA Renkhoff SE',
'model': 'Switch',
'model_id': None,
'name': 'HEIZUNG LINKS',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '499120',
'sw_version': None,
'via_device_id': <ANY>,
})
# ---
# name: test_switch_update
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by WMS WebControl pro API',
'friendly_name': 'HEIZUNG LINKS',
}),
'context': <ANY>,
'entity_id': 'switch.heizung_links',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@@ -0,0 +1,127 @@
"""Test the wmspro switch support."""
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.wmspro.const import DOMAIN
from homeassistant.components.wmspro.switch 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_switch_device(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hub_ping: AsyncMock,
mock_hub_configuration_prod_load_switch: AsyncMock,
mock_hub_status_prod_load_switch: AsyncMock,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test that a switch 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_load_switch.mock_calls) == 1
assert len(mock_hub_status_prod_load_switch.mock_calls) >= 2
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "499120")})
assert device_entry is not None
assert device_entry == snapshot
async def test_switch_update(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hub_ping: AsyncMock,
mock_hub_configuration_prod_load_switch: AsyncMock,
mock_hub_status_prod_load_switch: AsyncMock,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
) -> None:
"""Test that a switch 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_load_switch.mock_calls) == 1
assert len(mock_hub_status_prod_load_switch.mock_calls) >= 2
entity = hass.states.get("switch.heizung_links")
assert entity is not None
assert entity == snapshot
before = len(mock_hub_status_prod_load_switch.mock_calls)
# 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_load_switch.mock_calls) > before
async def test_switch_turn_on_and_off(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hub_ping: AsyncMock,
mock_hub_configuration_prod_load_switch: AsyncMock,
mock_hub_status_prod_load_switch: AsyncMock,
mock_action_call: AsyncMock,
) -> None:
"""Test that a switch 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_load_switch.mock_calls) == 1
assert len(mock_hub_status_prod_load_switch.mock_calls) >= 1
entity = hass.states.get("switch.heizung_links")
assert entity is not None
assert entity.state == STATE_OFF
with patch(
"wmspro.destination.Destination.refresh",
return_value=True,
):
before = len(mock_hub_status_prod_load_switch.mock_calls)
await hass.services.async_call(
Platform.SWITCH,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=True,
)
entity = hass.states.get("switch.heizung_links")
assert entity is not None
assert entity.state == STATE_ON
assert len(mock_hub_status_prod_load_switch.mock_calls) == before
with patch(
"wmspro.destination.Destination.refresh",
return_value=True,
):
before = len(mock_hub_status_prod_load_switch.mock_calls)
await hass.services.async_call(
Platform.SWITCH,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=True,
)
entity = hass.states.get("switch.heizung_links")
assert entity is not None
assert entity.state == STATE_OFF
assert len(mock_hub_status_prod_load_switch.mock_calls) == before