Add cover platform to Qbus integration (#147420)

* Add scene platform

* Add cover platform

* Refactor receiving state

* Fix wrong auto-merged code
This commit is contained in:
Thomas D 2025-06-25 15:38:43 +02:00 committed by GitHub
parent 977e8adbfb
commit 809aced9cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 609 additions and 43 deletions

View File

@ -13,7 +13,7 @@ from homeassistant.components.climate import (
HVACAction, HVACAction,
HVACMode, HVACMode,
) )
from homeassistant.components.mqtt import ReceiveMessage, client as mqtt from homeassistant.components.mqtt import client as mqtt
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import ServiceValidationError
@ -57,6 +57,8 @@ async def async_setup_entry(
class QbusClimate(QbusEntity, ClimateEntity): class QbusClimate(QbusEntity, ClimateEntity):
"""Representation of a Qbus climate entity.""" """Representation of a Qbus climate entity."""
_state_cls = QbusMqttThermoState
_attr_name = None _attr_name = None
_attr_hvac_modes = [HVACMode.HEAT] _attr_hvac_modes = [HVACMode.HEAT]
_attr_supported_features = ( _attr_supported_features = (
@ -128,14 +130,7 @@ class QbusClimate(QbusEntity, ClimateEntity):
await self._async_publish_output_state(state) await self._async_publish_output_state(state)
async def _state_received(self, msg: ReceiveMessage) -> None: async def _handle_state_received(self, state: QbusMqttThermoState) -> None:
state = self._message_factory.parse_output_state(
QbusMqttThermoState, msg.payload
)
if state is None:
return
if preset_mode := state.read_regime(): if preset_mode := state.read_regime():
self._attr_preset_mode = preset_mode self._attr_preset_mode = preset_mode
@ -155,8 +150,6 @@ class QbusClimate(QbusEntity, ClimateEntity):
assert self._request_state_debouncer is not None assert self._request_state_debouncer is not None
await self._request_state_debouncer.async_call() await self._request_state_debouncer.async_call()
self.async_schedule_update_ha_state()
def _set_hvac_action(self) -> None: def _set_hvac_action(self) -> None:
if self.target_temperature is None or self.current_temperature is None: if self.target_temperature is None or self.current_temperature is None:
self._attr_hvac_action = HVACAction.IDLE self._attr_hvac_action = HVACAction.IDLE

View File

@ -7,6 +7,7 @@ from homeassistant.const import Platform
DOMAIN: Final = "qbus" DOMAIN: Final = "qbus"
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.CLIMATE, Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT, Platform.LIGHT,
Platform.SCENE, Platform.SCENE,
Platform.SWITCH, Platform.SWITCH,

View File

@ -0,0 +1,193 @@
"""Support for Qbus cover."""
from typing import Any
from qbusmqttapi.const import (
KEY_PROPERTIES_SHUTTER_POSITION,
KEY_PROPERTIES_SLAT_POSITION,
)
from qbusmqttapi.discovery import QbusMqttOutput
from qbusmqttapi.state import QbusMqttShutterState, StateType
from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import QbusConfigEntry
from .entity import QbusEntity, add_new_outputs
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: QbusConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up cover entities."""
coordinator = entry.runtime_data
added_outputs: list[QbusMqttOutput] = []
def _check_outputs() -> None:
add_new_outputs(
coordinator,
added_outputs,
lambda output: output.type == "shutter",
QbusCover,
async_add_entities,
)
_check_outputs()
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
class QbusCover(QbusEntity, CoverEntity):
"""Representation of a Qbus cover entity."""
_state_cls = QbusMqttShutterState
_attr_name = None
_attr_supported_features: CoverEntityFeature
_attr_device_class = CoverDeviceClass.BLIND
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
"""Initialize cover entity."""
super().__init__(mqtt_output)
self._attr_assumed_state = False
self._attr_current_cover_position = 0
self._attr_current_cover_tilt_position = 0
self._attr_is_closed = True
self._attr_supported_features = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
)
if "shutterStop" in mqtt_output.actions:
self._attr_supported_features |= CoverEntityFeature.STOP
self._attr_assumed_state = True
if KEY_PROPERTIES_SHUTTER_POSITION in mqtt_output.properties:
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
if KEY_PROPERTIES_SLAT_POSITION in mqtt_output.properties:
self._attr_supported_features |= CoverEntityFeature.SET_TILT_POSITION
self._attr_supported_features |= CoverEntityFeature.OPEN_TILT
self._attr_supported_features |= CoverEntityFeature.CLOSE_TILT
self._target_shutter_position: int | None = None
self._target_slat_position: int | None = None
self._target_state: str | None = None
self._previous_state: str | None = None
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
state = QbusMqttShutterState(id=self._mqtt_output.id, type=StateType.STATE)
if self._attr_supported_features & CoverEntityFeature.SET_POSITION:
state.write_position(100)
else:
state.write_state("up")
await self._async_publish_output_state(state)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
state = QbusMqttShutterState(id=self._mqtt_output.id, type=StateType.STATE)
if self._attr_supported_features & CoverEntityFeature.SET_POSITION:
state.write_position(0)
if self._attr_supported_features & CoverEntityFeature.SET_TILT_POSITION:
state.write_slat_position(0)
else:
state.write_state("down")
await self._async_publish_output_state(state)
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
state = QbusMqttShutterState(id=self._mqtt_output.id, type=StateType.STATE)
state.write_state("stop")
await self._async_publish_output_state(state)
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
state = QbusMqttShutterState(id=self._mqtt_output.id, type=StateType.STATE)
state.write_position(int(kwargs[ATTR_POSITION]))
await self._async_publish_output_state(state)
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
state = QbusMqttShutterState(id=self._mqtt_output.id, type=StateType.STATE)
state.write_slat_position(50)
await self._async_publish_output_state(state)
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
state = QbusMqttShutterState(id=self._mqtt_output.id, type=StateType.STATE)
state.write_slat_position(0)
await self._async_publish_output_state(state)
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
state = QbusMqttShutterState(id=self._mqtt_output.id, type=StateType.STATE)
state.write_slat_position(int(kwargs[ATTR_TILT_POSITION]))
await self._async_publish_output_state(state)
async def _handle_state_received(self, state: QbusMqttShutterState) -> None:
output_state = state.read_state()
shutter_position = state.read_position()
slat_position = state.read_slat_position()
if output_state is not None:
self._previous_state = self._target_state
self._target_state = output_state
if shutter_position is not None:
self._target_shutter_position = shutter_position
if slat_position is not None:
self._target_slat_position = slat_position
self._update_is_closed()
self._update_cover_position()
self._update_tilt_position()
def _update_is_closed(self) -> None:
if self._attr_supported_features & CoverEntityFeature.SET_POSITION:
if self._attr_supported_features & CoverEntityFeature.SET_TILT_POSITION:
self._attr_is_closed = (
self._target_shutter_position == 0
and self._target_slat_position in (0, 100)
)
else:
self._attr_is_closed = self._target_shutter_position == 0
else:
self._attr_is_closed = (
self._previous_state == "down" and self._target_state == "stop"
)
def _update_cover_position(self) -> None:
self._attr_current_cover_position = (
self._target_shutter_position
if self._attr_supported_features & CoverEntityFeature.SET_POSITION
else None
)
def _update_tilt_position(self) -> None:
self._attr_current_cover_tilt_position = (
self._target_slat_position
if self._attr_supported_features & CoverEntityFeature.SET_TILT_POSITION
else None
)

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Callable from collections.abc import Callable
import re import re
from typing import Generic, TypeVar, cast
from qbusmqttapi.discovery import QbusMqttOutput from qbusmqttapi.discovery import QbusMqttOutput
from qbusmqttapi.factory import QbusMqttMessageFactory, QbusMqttTopicFactory from qbusmqttapi.factory import QbusMqttMessageFactory, QbusMqttTopicFactory
@ -20,6 +21,8 @@ from .coordinator import QbusControllerCoordinator
_REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$") _REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$")
StateT = TypeVar("StateT", bound=QbusMqttState)
def add_new_outputs( def add_new_outputs(
coordinator: QbusControllerCoordinator, coordinator: QbusControllerCoordinator,
@ -59,9 +62,11 @@ def create_main_device_identifier(mqtt_output: QbusMqttOutput) -> tuple[str, str
return (DOMAIN, format_mac(mqtt_output.device.mac)) return (DOMAIN, format_mac(mqtt_output.device.mac))
class QbusEntity(Entity, ABC): class QbusEntity(Entity, Generic[StateT], ABC):
"""Representation of a Qbus entity.""" """Representation of a Qbus entity."""
_state_cls: type[StateT] = cast(type[StateT], QbusMqttState)
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_should_poll = False _attr_should_poll = False
@ -97,9 +102,16 @@ class QbusEntity(Entity, ABC):
) )
) )
@abstractmethod
async def _state_received(self, msg: ReceiveMessage) -> None: async def _state_received(self, msg: ReceiveMessage) -> None:
pass state = self._message_factory.parse_output_state(self._state_cls, msg.payload)
if isinstance(state, self._state_cls):
await self._handle_state_received(state)
self.async_schedule_update_ha_state()
@abstractmethod
async def _handle_state_received(self, state: StateT) -> None:
raise NotImplementedError
async def _async_publish_output_state(self, state: QbusMqttState) -> None: async def _async_publish_output_state(self, state: QbusMqttState) -> None:
request = self._message_factory.create_set_output_state_request( request = self._message_factory.create_set_output_state_request(

View File

@ -6,7 +6,6 @@ from qbusmqttapi.discovery import QbusMqttOutput
from qbusmqttapi.state import QbusMqttAnalogState, StateType from qbusmqttapi.state import QbusMqttAnalogState, StateType
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.color import brightness_to_value, value_to_brightness from homeassistant.util.color import brightness_to_value, value_to_brightness
@ -43,6 +42,8 @@ async def async_setup_entry(
class QbusLight(QbusEntity, LightEntity): class QbusLight(QbusEntity, LightEntity):
"""Representation of a Qbus light entity.""" """Representation of a Qbus light entity."""
_state_cls = QbusMqttAnalogState
_attr_name = None _attr_name = None
_attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
_attr_color_mode = ColorMode.BRIGHTNESS _attr_color_mode = ColorMode.BRIGHTNESS
@ -57,17 +58,11 @@ class QbusLight(QbusEntity, LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on.""" """Turn the entity on."""
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
percentage: int | None = None
on: bool | None = None
state = QbusMqttAnalogState(id=self._mqtt_output.id) state = QbusMqttAnalogState(id=self._mqtt_output.id)
if brightness is None: if brightness is None:
on = True
state.type = StateType.ACTION state.type = StateType.ACTION
state.write_on_off(on) state.write_on_off(on=True)
else: else:
percentage = round(brightness_to_value((1, 100), brightness)) percentage = round(brightness_to_value((1, 100), brightness))
@ -83,16 +78,10 @@ class QbusLight(QbusEntity, LightEntity):
await self._async_publish_output_state(state) await self._async_publish_output_state(state)
async def _state_received(self, msg: ReceiveMessage) -> None: async def _handle_state_received(self, state: QbusMqttAnalogState) -> None:
output = self._message_factory.parse_output_state( percentage = round(state.read_percentage())
QbusMqttAnalogState, msg.payload self._set_state(percentage)
)
if output is not None: def _set_state(self, percentage: int) -> None:
percentage = round(output.read_percentage())
self._set_state(percentage)
self.async_schedule_update_ha_state()
def _set_state(self, percentage: int = 0) -> None:
self._attr_is_on = percentage > 0 self._attr_is_on = percentage > 0
self._attr_brightness = value_to_brightness((1, 100), percentage) self._attr_brightness = value_to_brightness((1, 100), percentage)

View File

@ -5,7 +5,6 @@ from typing import Any
from qbusmqttapi.discovery import QbusMqttOutput from qbusmqttapi.discovery import QbusMqttOutput
from qbusmqttapi.state import QbusMqttState, StateAction, StateType from qbusmqttapi.state import QbusMqttState, StateAction, StateType
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.components.scene import Scene from homeassistant.components.scene import Scene
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -61,6 +60,6 @@ class QbusScene(QbusEntity, Scene):
) )
await self._async_publish_output_state(state) await self._async_publish_output_state(state)
async def _state_received(self, msg: ReceiveMessage) -> None: async def _handle_state_received(self, state: QbusMqttState) -> None:
# Nothing to do # Nothing to do
pass pass

View File

@ -5,7 +5,6 @@ from typing import Any
from qbusmqttapi.discovery import QbusMqttOutput from qbusmqttapi.discovery import QbusMqttOutput
from qbusmqttapi.state import QbusMqttOnOffState, StateType from qbusmqttapi.state import QbusMqttOnOffState, StateType
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@ -42,6 +41,8 @@ async def async_setup_entry(
class QbusSwitch(QbusEntity, SwitchEntity): class QbusSwitch(QbusEntity, SwitchEntity):
"""Representation of a Qbus switch entity.""" """Representation of a Qbus switch entity."""
_state_cls = QbusMqttOnOffState
_attr_name = None _attr_name = None
_attr_device_class = SwitchDeviceClass.SWITCH _attr_device_class = SwitchDeviceClass.SWITCH
@ -66,11 +67,5 @@ class QbusSwitch(QbusEntity, SwitchEntity):
await self._async_publish_output_state(state) await self._async_publish_output_state(state)
async def _state_received(self, msg: ReceiveMessage) -> None: async def _handle_state_received(self, state: QbusMqttOnOffState) -> None:
output = self._message_factory.parse_output_state( self._attr_is_on = state.read_value()
QbusMqttOnOffState, msg.payload
)
if output is not None:
self._attr_is_on = output.read_value()
self.async_schedule_update_ha_state()

View File

@ -1,10 +1,13 @@
"""Test fixtures for qbus.""" """Test fixtures for qbus."""
from collections.abc import Generator
import json import json
from unittest.mock import AsyncMock, patch
import pytest import pytest
from homeassistant.components.qbus.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.components.qbus.const import CONF_SERIAL_NUMBER, DOMAIN
from homeassistant.components.qbus.entity import QbusEntity
from homeassistant.const import CONF_ID from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.util.json import JsonObjectType from homeassistant.util.json import JsonObjectType
@ -16,6 +19,7 @@ from tests.common import (
async_fire_mqtt_message, async_fire_mqtt_message,
load_json_object_fixture, load_json_object_fixture,
) )
from tests.typing import MqttMockHAClient
@pytest.fixture @pytest.fixture
@ -39,9 +43,17 @@ def payload_config() -> JsonObjectType:
return load_json_object_fixture(FIXTURE_PAYLOAD_CONFIG, DOMAIN) return load_json_object_fixture(FIXTURE_PAYLOAD_CONFIG, DOMAIN)
@pytest.fixture
def mock_publish_state() -> Generator[AsyncMock]:
"""Return a mocked publish state call."""
with patch.object(QbusEntity, "_async_publish_output_state") as mock:
yield mock
@pytest.fixture @pytest.fixture
async def setup_integration( async def setup_integration(
hass: HomeAssistant, hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
payload_config: JsonObjectType, payload_config: JsonObjectType,
) -> None: ) -> None:

View File

@ -112,6 +112,77 @@
"active": null "active": null
}, },
"properties": {} "properties": {}
},
{
"id": "UL30",
"location": "Guest bedroom",
"locationId": 0,
"name": "CURTAINS",
"originalName": "CURTAINS",
"refId": "000001/108",
"type": "shutter",
"actions": {
"shutterDown": null,
"shutterStop": null,
"shutterUp": null
},
"properties": {
"state": {
"enumValues": ["up", "stop", "down"],
"read": true,
"type": "enumString",
"write": false
}
}
},
{
"actions": {
"shutterDown": null,
"shutterUp": null,
"slatDown": null,
"slatUp": null
},
"id": "UL31",
"location": "Living",
"locationId": 8,
"name": "SLATS",
"originalName": "SLATS",
"properties": {
"shutterPosition": {
"read": true,
"step": 0.10000000000000001,
"type": "percent",
"write": true
},
"slatPosition": {
"read": true,
"step": 0.10000000000000001,
"type": "percent",
"write": true
}
},
"refId": "000001/8",
"type": "shutter"
},
{
"actions": {
"shutterDown": null,
"shutterUp": null
},
"id": "UL32",
"location": "Kitchen",
"locationId": 8,
"name": "BLINDS",
"originalName": "BLINDS",
"properties": {
"shutterPosition": {
"read": true,
"type": "percent",
"write": true
}
},
"refId": "000001/4",
"type": "shutter"
} }
] ]
} }

View File

@ -0,0 +1,301 @@
"""Test Qbus cover entities."""
from unittest.mock import AsyncMock
from qbusmqttapi.state import QbusMqttShutterState
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION,
ATTR_CURRENT_TILT_POSITION,
ATTR_POSITION,
ATTR_TILT_POSITION,
DOMAIN as COVER_DOMAIN,
CoverEntityFeature,
CoverState,
)
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,
)
from homeassistant.core import HomeAssistant
from tests.common import async_fire_mqtt_message
_PAYLOAD_UDS_STATE_CLOSED = '{"id":"UL30","properties":{"state":"down"},"type":"state"}'
_PAYLOAD_UDS_STATE_OPENED = '{"id":"UL30","properties":{"state":"up"},"type":"state"}'
_PAYLOAD_UDS_STATE_STOPPED = (
'{"id":"UL30","properties":{"state":"stop"},"type":"state"}'
)
_PAYLOAD_POS_STATE_CLOSED = (
'{"id":"UL32","properties":{"shutterPosition":0},"type":"event"}'
)
_PAYLOAD_POS_STATE_OPENED = (
'{"id":"UL32","properties":{"shutterPosition":100},"type":"event"}'
)
_PAYLOAD_POS_STATE_POSITION = (
'{"id":"UL32","properties":{"shutterPosition":50},"type":"event"}'
)
_PAYLOAD_SLAT_STATE_CLOSED = (
'{"id":"UL31","properties":{"slatPosition":0},"type":"event"}'
)
_PAYLOAD_SLAT_STATE_FULLY_CLOSED = (
'{"id":"UL31","properties":{"slatPosition":0,"shutterPosition":0},"type":"event"}'
)
_PAYLOAD_SLAT_STATE_OPENED = (
'{"id":"UL31","properties":{"slatPosition":50},"type":"event"}'
)
_PAYLOAD_SLAT_STATE_POSITION = (
'{"id":"UL31","properties":{"slatPosition":75},"type":"event"}'
)
_TOPIC_UDS_STATE = "cloudapp/QBUSMQTTGW/UL1/UL30/state"
_TOPIC_POS_STATE = "cloudapp/QBUSMQTTGW/UL1/UL32/state"
_TOPIC_SLAT_STATE = "cloudapp/QBUSMQTTGW/UL1/UL31/state"
_ENTITY_ID_UDS = "cover.curtains"
_ENTITY_ID_POS = "cover.blinds"
_ENTITY_ID_SLAT = "cover.slats"
async def test_cover_up_down_stop(
hass: HomeAssistant, setup_integration: None, mock_publish_state: AsyncMock
) -> None:
"""Test cover up, down and stop."""
attributes = hass.states.get(_ENTITY_ID_UDS).attributes
assert attributes.get("supported_features") == (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
)
# Cover open
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: _ENTITY_ID_UDS},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_state() == "up"
# Simulate response
async_fire_mqtt_message(hass, _TOPIC_UDS_STATE, _PAYLOAD_UDS_STATE_OPENED)
await hass.async_block_till_done()
assert hass.states.get(_ENTITY_ID_UDS).state == CoverState.OPEN
# Cover close
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: _ENTITY_ID_UDS},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_state() == "down"
# Simulate response
async_fire_mqtt_message(hass, _TOPIC_UDS_STATE, _PAYLOAD_UDS_STATE_CLOSED)
await hass.async_block_till_done()
assert hass.states.get(_ENTITY_ID_UDS).state == CoverState.OPEN
# Cover stop
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_STOP_COVER,
{ATTR_ENTITY_ID: _ENTITY_ID_UDS},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_state() == "stop"
# Simulate response
async_fire_mqtt_message(hass, _TOPIC_UDS_STATE, _PAYLOAD_UDS_STATE_STOPPED)
await hass.async_block_till_done()
assert hass.states.get(_ENTITY_ID_UDS).state == CoverState.CLOSED
async def test_cover_position(
hass: HomeAssistant, setup_integration: None, mock_publish_state: AsyncMock
) -> None:
"""Test cover positions."""
attributes = hass.states.get(_ENTITY_ID_POS).attributes
assert attributes.get("supported_features") == (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)
# Cover open
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: _ENTITY_ID_POS},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_position() == 100
async_fire_mqtt_message(hass, _TOPIC_POS_STATE, _PAYLOAD_POS_STATE_OPENED)
await hass.async_block_till_done()
entity_state = hass.states.get(_ENTITY_ID_POS)
assert entity_state.state == CoverState.OPEN
assert entity_state.attributes[ATTR_CURRENT_POSITION] == 100
# Cover position
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_SET_COVER_POSITION,
{ATTR_ENTITY_ID: _ENTITY_ID_POS, ATTR_POSITION: 50},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_position() == 50
async_fire_mqtt_message(hass, _TOPIC_POS_STATE, _PAYLOAD_POS_STATE_POSITION)
await hass.async_block_till_done()
entity_state = hass.states.get(_ENTITY_ID_POS)
assert entity_state.state == CoverState.OPEN
assert entity_state.attributes[ATTR_CURRENT_POSITION] == 50
# Cover close
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: _ENTITY_ID_POS},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_position() == 0
async_fire_mqtt_message(hass, _TOPIC_POS_STATE, _PAYLOAD_POS_STATE_CLOSED)
await hass.async_block_till_done()
entity_state = hass.states.get(_ENTITY_ID_POS)
assert entity_state.state == CoverState.CLOSED
assert entity_state.attributes[ATTR_CURRENT_POSITION] == 0
async def test_cover_slats(
hass: HomeAssistant, setup_integration: None, mock_publish_state: AsyncMock
) -> None:
"""Test cover slats."""
attributes = hass.states.get(_ENTITY_ID_SLAT).attributes
assert attributes.get("supported_features") == (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
| CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
# Start with a fully closed cover
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: _ENTITY_ID_SLAT},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_position() == 0
assert publish_state.read_slat_position() == 0
async_fire_mqtt_message(hass, _TOPIC_SLAT_STATE, _PAYLOAD_SLAT_STATE_FULLY_CLOSED)
await hass.async_block_till_done()
entity_state = hass.states.get(_ENTITY_ID_SLAT)
assert entity_state.state == CoverState.CLOSED
assert entity_state.attributes[ATTR_CURRENT_POSITION] == 0
assert entity_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0
# Slat open
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER_TILT,
{ATTR_ENTITY_ID: _ENTITY_ID_SLAT},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_slat_position() == 50
async_fire_mqtt_message(hass, _TOPIC_SLAT_STATE, _PAYLOAD_SLAT_STATE_OPENED)
await hass.async_block_till_done()
entity_state = hass.states.get(_ENTITY_ID_SLAT)
assert entity_state.state == CoverState.OPEN
assert entity_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50
# SLat position
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_SET_COVER_TILT_POSITION,
{ATTR_ENTITY_ID: _ENTITY_ID_SLAT, ATTR_TILT_POSITION: 75},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_slat_position() == 75
async_fire_mqtt_message(hass, _TOPIC_SLAT_STATE, _PAYLOAD_SLAT_STATE_POSITION)
await hass.async_block_till_done()
entity_state = hass.states.get(_ENTITY_ID_SLAT)
assert entity_state.state == CoverState.OPEN
assert entity_state.attributes[ATTR_CURRENT_TILT_POSITION] == 75
# Slat close
mock_publish_state.reset_mock()
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER_TILT,
{ATTR_ENTITY_ID: _ENTITY_ID_SLAT},
blocking=True,
)
publish_state = _get_publish_state(mock_publish_state)
assert publish_state.read_slat_position() == 0
async_fire_mqtt_message(hass, _TOPIC_SLAT_STATE, _PAYLOAD_SLAT_STATE_CLOSED)
await hass.async_block_till_done()
entity_state = hass.states.get(_ENTITY_ID_SLAT)
assert entity_state.state == CoverState.CLOSED
assert entity_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0
def _get_publish_state(mock_publish_state: AsyncMock) -> QbusMqttShutterState:
assert mock_publish_state.call_count == 1
state = mock_publish_state.call_args.args[0]
assert isinstance(state, QbusMqttShutterState)
return state