mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
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:
parent
977e8adbfb
commit
809aced9cc
@ -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
|
||||
|
@ -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,
|
||||
|
193
homeassistant/components/qbus/cover.py
Normal file
193
homeassistant/components/qbus/cover.py
Normal 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
|
||||
)
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
301
tests/components/qbus/test_cover.py
Normal file
301
tests/components/qbus/test_cover.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user