mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
bosch_shc: create cover entities for micromodule shutters and blinds (#181416)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
323c3faad1
commit
bb76014aad
@@ -2,10 +2,11 @@
|
||||
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from boschshcpy import SHCShutterControl, ShutterControlService
|
||||
from boschshcpy import SHCMicromoduleBlinds, SHCShutterControl, ShutterControlService
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
ATTR_TILT_POSITION,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
@@ -38,7 +39,19 @@ async def async_setup_entry(
|
||||
parent_id=shc_info.unique_id,
|
||||
entry_id=config_entry.entry_id,
|
||||
)
|
||||
for cover in session.device_helper.shutter_controls
|
||||
for cover in (
|
||||
*session.device_helper.shutter_controls,
|
||||
*session.device_helper.micromodule_shutter_controls,
|
||||
)
|
||||
)
|
||||
async_add_entities(
|
||||
BlindsControlCover(
|
||||
hass=hass,
|
||||
device=blind,
|
||||
parent_id=shc_info.unique_id,
|
||||
entry_id=config_entry.entry_id,
|
||||
)
|
||||
for blind in session.device_helper.micromodule_blinds
|
||||
)
|
||||
|
||||
|
||||
@@ -46,7 +59,6 @@ class ShutterControlCover(SHCEntity, CoverEntity):
|
||||
"""Representation of a SHC shutter control device."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_device_class = CoverDeviceClass.SHUTTER
|
||||
_device: SHCShutterControl
|
||||
_attr_supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
@@ -55,6 +67,14 @@ class ShutterControlCover(SHCEntity, CoverEntity):
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def device_class(self) -> CoverDeviceClass:
|
||||
"""Return the device class."""
|
||||
if self._device.device_model == "MICROMODULE_AWNING":
|
||||
return CoverDeviceClass.AWNING
|
||||
return CoverDeviceClass.SHUTTER
|
||||
|
||||
@property
|
||||
@override
|
||||
def current_cover_position(self) -> int:
|
||||
@@ -99,3 +119,51 @@ class ShutterControlCover(SHCEntity, CoverEntity):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
self._device.level = position / 100.0
|
||||
|
||||
|
||||
class BlindsControlCover(ShutterControlCover):
|
||||
"""Representation of a SHC micromodule blinds cover device."""
|
||||
|
||||
_device: SHCMicromoduleBlinds
|
||||
_attr_supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.STOP
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
| CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.SET_TILT_POSITION
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def device_class(self) -> CoverDeviceClass:
|
||||
"""Return the device class."""
|
||||
return CoverDeviceClass.BLIND
|
||||
|
||||
@override
|
||||
def stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
self._device.stop_blinds()
|
||||
|
||||
@property
|
||||
@override
|
||||
def current_cover_tilt_position(self) -> int:
|
||||
"""Return the current cover tilt position."""
|
||||
return round((1.0 - self._device.current_angle) * 100.0)
|
||||
|
||||
@override
|
||||
def open_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Open the cover tilt."""
|
||||
self._device.target_angle = 0.0
|
||||
|
||||
@override
|
||||
def close_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Close the cover tilt."""
|
||||
self._device.target_angle = 1.0
|
||||
|
||||
@override
|
||||
def set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover tilt to a specific position."""
|
||||
tilt_position = kwargs[ATTR_TILT_POSITION]
|
||||
self._device.target_angle = 1.0 - (tilt_position / 100.0)
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from boschshcpy import (
|
||||
SHCLightSwitchBSM,
|
||||
SHCMicromoduleShutterControl,
|
||||
SHCSmartPlug,
|
||||
SHCSmartPlugCompact,
|
||||
SHCThermostat,
|
||||
@@ -47,7 +48,7 @@ class SHCSensorEntityDescription[_DeviceT: SHCDevice](SensorEntityDescription):
|
||||
attributes_fn: Callable[[_DeviceT], dict[str, Any]] | None = None
|
||||
|
||||
|
||||
_PowerMeterDevice = SHCSmartPlug | SHCLightSwitchBSM
|
||||
_PowerMeterDevice = SHCSmartPlug | SHCLightSwitchBSM | SHCMicromoduleShutterControl
|
||||
|
||||
TEMPERATURE_SENSOR = "temperature"
|
||||
HUMIDITY_SENSOR = "humidity"
|
||||
@@ -263,6 +264,8 @@ async def async_setup_entry(
|
||||
power_meter_devices: list[_PowerMeterDevice] = [
|
||||
*session.device_helper.smart_plugs,
|
||||
*session.device_helper.light_switches_bsm,
|
||||
*session.device_helper.micromodule_shutter_controls,
|
||||
*session.device_helper.micromodule_blinds,
|
||||
]
|
||||
entities.extend(
|
||||
SHCSensor(
|
||||
|
||||
@@ -10,8 +10,11 @@ from boschshcpy import (
|
||||
PowerSwitchService,
|
||||
SHCBatteryDevice,
|
||||
SHCLightSwitchBSM,
|
||||
SHCMicromoduleBlinds,
|
||||
SHCMicromoduleRelay,
|
||||
SHCShutterControl,
|
||||
SHCThermostat,
|
||||
ShutterControlService,
|
||||
ThermostatService,
|
||||
)
|
||||
import pytest
|
||||
@@ -64,6 +67,7 @@ _EMPTY_DEVICE_BUCKETS: dict[str, list[Any]] = {
|
||||
"roomthermostats",
|
||||
"shutter_contacts",
|
||||
"shutter_contacts2",
|
||||
"shutter_controls",
|
||||
"smart_plugs",
|
||||
"smart_plugs_compact",
|
||||
"smoke_detectors",
|
||||
@@ -134,6 +138,53 @@ def battery_only_device(
|
||||
return device
|
||||
|
||||
|
||||
def shutter_control_device(
|
||||
device_id: str = "hdm:ZigBee:shutter1",
|
||||
name: str = "Shutter",
|
||||
device_model: str = "BBL",
|
||||
level: float = 1.0,
|
||||
operation_state: ShutterControlService.State = ShutterControlService.State.STOPPED,
|
||||
) -> SHCShutterControl:
|
||||
"""Build a minimal device double for the shutter_controls/micromodule_shutter_controls buckets."""
|
||||
device = create_autospec(SHCShutterControl, instance=True, spec_set=True)
|
||||
device.name = name
|
||||
device.id = device_id
|
||||
device.root_device_id = "test-mac"
|
||||
device.serial = f"serial-{device_id}"
|
||||
device.manufacturer = "Bosch"
|
||||
device.device_model = device_model
|
||||
device.device_services = []
|
||||
device.deleted = False
|
||||
device.status = "AVAILABLE"
|
||||
device.level = level
|
||||
device.operation_state = operation_state
|
||||
return device
|
||||
|
||||
|
||||
def micromodule_blinds_device(
|
||||
device_id: str = "hdm:ZigBee:blinds1",
|
||||
name: str = "Blinds",
|
||||
level: float = 1.0,
|
||||
current_angle: float = 0.0,
|
||||
operation_state: ShutterControlService.State = ShutterControlService.State.STOPPED,
|
||||
) -> SHCMicromoduleBlinds:
|
||||
"""Build a minimal device double for the micromodule_blinds bucket."""
|
||||
device = create_autospec(SHCMicromoduleBlinds, instance=True, spec_set=True)
|
||||
device.name = name
|
||||
device.id = device_id
|
||||
device.root_device_id = "test-mac"
|
||||
device.serial = f"serial-{device_id}"
|
||||
device.manufacturer = "Bosch"
|
||||
device.device_model = "MICROMODULE_BLINDS"
|
||||
device.device_services = []
|
||||
device.deleted = False
|
||||
device.status = "AVAILABLE"
|
||||
device.level = level
|
||||
device.current_angle = current_angle
|
||||
device.operation_state = operation_state
|
||||
return device
|
||||
|
||||
|
||||
def thermostat_device(
|
||||
device_id: str = "hdm:ZigBee:thermostat1",
|
||||
name: str = "Thermostat",
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"""Tests for the Bosch SHC cover platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
ATTR_TILT_POSITION,
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_CLOSE_COVER,
|
||||
SERVICE_CLOSE_COVER_TILT,
|
||||
SERVICE_OPEN_COVER,
|
||||
SERVICE_OPEN_COVER_TILT,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_SET_COVER_TILT_POSITION,
|
||||
SERVICE_STOP_COVER,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import (
|
||||
micromodule_blinds_device,
|
||||
setup_integration,
|
||||
shutter_control_device,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def platforms() -> Generator[None]:
|
||||
"""Restrict bosch_shc setup to the cover platform."""
|
||||
with patch("homeassistant.components.bosch_shc.PLATFORMS", [Platform.COVER]):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[{"shutter_controls": [shutter_control_device(device_model="BBL", level=0.75)]}],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_bbl_shutter_control(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""A plain BBL shutter is exposed as a shutter-class cover."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = mock_session.device_helper.shutter_controls[0]
|
||||
|
||||
state = hass.states.get("cover.shutter")
|
||||
assert state is not None
|
||||
assert state.attributes["device_class"] == "shutter"
|
||||
assert state.attributes["current_position"] == 75
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: "cover.shutter"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.level == 1.0
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
{ATTR_ENTITY_ID: "cover.shutter"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.level == 0.0
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
{ATTR_ENTITY_ID: "cover.shutter", ATTR_POSITION: 42},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.level == pytest.approx(0.42)
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_STOP_COVER,
|
||||
{ATTR_ENTITY_ID: "cover.shutter"},
|
||||
blocking=True,
|
||||
)
|
||||
device.stop.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_buckets", "expected_device_class"),
|
||||
[
|
||||
pytest.param(
|
||||
{
|
||||
"micromodule_shutter_controls": [
|
||||
shutter_control_device(device_model="MICROMODULE_SHUTTER")
|
||||
]
|
||||
},
|
||||
"shutter",
|
||||
id="micromodule_shutter",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"micromodule_shutter_controls": [
|
||||
shutter_control_device(device_model="MICROMODULE_AWNING")
|
||||
]
|
||||
},
|
||||
"awning",
|
||||
id="micromodule_awning",
|
||||
),
|
||||
],
|
||||
indirect=["device_buckets"],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_micromodule_shutter_device_class(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
expected_device_class: str,
|
||||
) -> None:
|
||||
"""A micromodule shutter/awning device (#181407) gets the right device class."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("cover.shutter")
|
||||
assert state is not None
|
||||
assert state.attributes["device_class"] == expected_device_class
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_buckets",
|
||||
[
|
||||
{
|
||||
"micromodule_blinds": [
|
||||
micromodule_blinds_device(level=0.6, current_angle=0.25)
|
||||
]
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_session")
|
||||
async def test_micromodule_blinds_tilt(
|
||||
hass: HomeAssistant,
|
||||
mock_session: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Micromodule blinds (#181407) expose tilt controls in addition to lift."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = mock_session.device_helper.micromodule_blinds[0]
|
||||
|
||||
state = hass.states.get("cover.blinds")
|
||||
assert state is not None
|
||||
assert state.attributes["device_class"] == "blind"
|
||||
assert state.attributes["current_position"] == 60
|
||||
assert state.attributes["current_tilt_position"] == 75
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER_TILT,
|
||||
{ATTR_ENTITY_ID: "cover.blinds"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.target_angle == 0.0
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER_TILT,
|
||||
{ATTR_ENTITY_ID: "cover.blinds"},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.target_angle == 1.0
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_TILT_POSITION,
|
||||
{ATTR_ENTITY_ID: "cover.blinds", ATTR_TILT_POSITION: 30},
|
||||
blocking=True,
|
||||
)
|
||||
assert device.target_angle == pytest.approx(0.7)
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_STOP_COVER,
|
||||
{ATTR_ENTITY_ID: "cover.blinds"},
|
||||
blocking=True,
|
||||
)
|
||||
device.stop_blinds.assert_called_once()
|
||||
Reference in New Issue
Block a user