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:
@@ -20,6 +20,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.SCENE,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
type WebControlProConfigEntry = ConfigEntry[WebControlPro]
|
||||
|
||||
62
homeassistant/components/wmspro/switch.py
Normal file
62
homeassistant/components/wmspro/switch.py
Normal 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
|
||||
)
|
||||
@@ -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."""
|
||||
|
||||
249
tests/components/wmspro/fixtures/config_prod_load_switch.json
Normal file
249
tests/components/wmspro/fixtures/config_prod_load_switch.json
Normal 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": []
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
46
tests/components/wmspro/snapshots/test_switch.ambr
Normal file
46
tests/components/wmspro/snapshots/test_switch.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
||||
127
tests/components/wmspro/test_switch.py
Normal file
127
tests/components/wmspro/test_switch.py
Normal 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
|
||||
Reference in New Issue
Block a user