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

View File

@ -7,6 +7,7 @@ from homeassistant.const import Platform
DOMAIN: Final = "qbus"
PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.SCENE,
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 collections.abc import Callable
import re
from typing import Generic, TypeVar, cast
from qbusmqttapi.discovery import QbusMqttOutput
from qbusmqttapi.factory import QbusMqttMessageFactory, QbusMqttTopicFactory
@ -20,6 +21,8 @@ from .coordinator import QbusControllerCoordinator
_REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$")
StateT = TypeVar("StateT", bound=QbusMqttState)
def add_new_outputs(
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))
class QbusEntity(Entity, ABC):
class QbusEntity(Entity, Generic[StateT], ABC):
"""Representation of a Qbus entity."""
_state_cls: type[StateT] = cast(type[StateT], QbusMqttState)
_attr_has_entity_name = True
_attr_should_poll = False
@ -97,9 +102,16 @@ class QbusEntity(Entity, ABC):
)
)
@abstractmethod
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:
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 homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.color import brightness_to_value, value_to_brightness
@ -43,6 +42,8 @@ async def async_setup_entry(
class QbusLight(QbusEntity, LightEntity):
"""Representation of a Qbus light entity."""
_state_cls = QbusMqttAnalogState
_attr_name = None
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
_attr_color_mode = ColorMode.BRIGHTNESS
@ -57,17 +58,11 @@ class QbusLight(QbusEntity, LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
percentage: int | None = None
on: bool | None = None
state = QbusMqttAnalogState(id=self._mqtt_output.id)
if brightness is None:
on = True
state.type = StateType.ACTION
state.write_on_off(on)
state.write_on_off(on=True)
else:
percentage = round(brightness_to_value((1, 100), brightness))
@ -83,16 +78,10 @@ class QbusLight(QbusEntity, LightEntity):
await self._async_publish_output_state(state)
async def _state_received(self, msg: ReceiveMessage) -> None:
output = self._message_factory.parse_output_state(
QbusMqttAnalogState, msg.payload
)
async def _handle_state_received(self, state: QbusMqttAnalogState) -> None:
percentage = round(state.read_percentage())
self._set_state(percentage)
if output is not None:
percentage = round(output.read_percentage())
self._set_state(percentage)
self.async_schedule_update_ha_state()
def _set_state(self, percentage: int = 0) -> None:
def _set_state(self, percentage: int) -> None:
self._attr_is_on = percentage > 0
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.state import QbusMqttState, StateAction, StateType
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.components.scene import Scene
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@ -61,6 +60,6 @@ class QbusScene(QbusEntity, Scene):
)
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
pass

View File

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

View File

@ -1,10 +1,13 @@
"""Test fixtures for qbus."""
from collections.abc import Generator
import json
from unittest.mock import AsyncMock, patch
import pytest
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.core import HomeAssistant
from homeassistant.util.json import JsonObjectType
@ -16,6 +19,7 @@ from tests.common import (
async_fire_mqtt_message,
load_json_object_fixture,
)
from tests.typing import MqttMockHAClient
@pytest.fixture
@ -39,9 +43,17 @@ def payload_config() -> JsonObjectType:
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
async def setup_integration(
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
mock_config_entry: MockConfigEntry,
payload_config: JsonObjectType,
) -> None:

View File

@ -112,6 +112,77 @@
"active": null
},
"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