mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Add Shelly gen2 cover support (#67705)
* Add Shelly gen2 cover support * Make status property
This commit is contained in:
parent
0b7b1baf30
commit
2d4d18ab90
@ -86,6 +86,7 @@ BLOCK_SLEEPING_PLATFORMS: Final = [
|
||||
RPC_PLATFORMS: Final = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
|
@ -18,15 +18,28 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BlockDeviceWrapper
|
||||
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN
|
||||
from .entity import ShellyBlockEntity
|
||||
from . import BlockDeviceWrapper, RpcDeviceWrapper
|
||||
from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC
|
||||
from .entity import ShellyBlockEntity, ShellyRpcEntity
|
||||
from .utils import get_device_entry_gen, get_rpc_key_ids
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switches for device."""
|
||||
if get_device_entry_gen(config_entry) == 2:
|
||||
return await async_setup_rpc_entry(hass, config_entry, async_add_entities)
|
||||
|
||||
return await async_setup_block_entry(hass, config_entry, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_block_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up cover for device."""
|
||||
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK]
|
||||
@ -35,16 +48,32 @@ async def async_setup_entry(
|
||||
if not blocks:
|
||||
return
|
||||
|
||||
async_add_entities(ShellyCover(wrapper, block) for block in blocks)
|
||||
async_add_entities(BlockShellyCover(wrapper, block) for block in blocks)
|
||||
|
||||
|
||||
class ShellyCover(ShellyBlockEntity, CoverEntity):
|
||||
"""Switch that controls a cover block on Shelly devices."""
|
||||
async def async_setup_rpc_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entities for RPC device."""
|
||||
wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC]
|
||||
|
||||
cover_key_ids = get_rpc_key_ids(wrapper.device.status, "cover")
|
||||
|
||||
if not cover_key_ids:
|
||||
return
|
||||
|
||||
async_add_entities(RpcShellyCover(wrapper, id_) for id_ in cover_key_ids)
|
||||
|
||||
|
||||
class BlockShellyCover(ShellyBlockEntity, CoverEntity):
|
||||
"""Entity that controls a cover on block based Shelly devices."""
|
||||
|
||||
_attr_device_class = CoverDeviceClass.SHUTTER
|
||||
|
||||
def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None:
|
||||
"""Initialize light."""
|
||||
"""Initialize block cover."""
|
||||
super().__init__(wrapper, block)
|
||||
self.control_result: dict[str, Any] | None = None
|
||||
self._attr_supported_features: int = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
@ -110,3 +139,61 @@ class ShellyCover(ShellyBlockEntity, CoverEntity):
|
||||
"""When device updates, clear control result that overrides state."""
|
||||
self.control_result = None
|
||||
super()._update_callback()
|
||||
|
||||
|
||||
class RpcShellyCover(ShellyRpcEntity, CoverEntity):
|
||||
"""Entity that controls a cover on RPC based Shelly devices."""
|
||||
|
||||
_attr_device_class = CoverDeviceClass.SHUTTER
|
||||
|
||||
def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None:
|
||||
"""Initialize rpc cover."""
|
||||
super().__init__(wrapper, f"cover:{id_}")
|
||||
self._id = id_
|
||||
self._attr_supported_features: int = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
if self.status["pos_control"]:
|
||||
self._attr_supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""If cover is closed."""
|
||||
if not self.status["pos_control"]:
|
||||
return None
|
||||
|
||||
return cast(bool, self.status["state"] == "closed")
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int | None:
|
||||
"""Position of the cover."""
|
||||
if not self.status["pos_control"]:
|
||||
return None
|
||||
|
||||
return cast(int, self.status["current_pos"])
|
||||
|
||||
@property
|
||||
def is_closing(self) -> bool:
|
||||
"""Return if the cover is closing."""
|
||||
return cast(bool, self.status["state"] == "closing")
|
||||
|
||||
@property
|
||||
def is_opening(self) -> bool:
|
||||
"""Return if the cover is opening."""
|
||||
return cast(bool, self.status["state"] == "opening")
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
await self.call_rpc("Cover.Close", {"id": self._id})
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open cover."""
|
||||
await self.call_rpc("Cover.Open", {"id": self._id})
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
await self.call_rpc(
|
||||
"Cover.GoToPosition", {"id": self._id, "pos": kwargs[ATTR_POSITION]}
|
||||
)
|
||||
|
||||
async def async_stop_cover(self, **_kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
await self.call_rpc("Cover.Stop", {"id": self._id})
|
||||
|
@ -351,6 +351,11 @@ class ShellyRpcEntity(entity.Entity):
|
||||
"""Available."""
|
||||
return self.wrapper.device.connected
|
||||
|
||||
@property
|
||||
def status(self) -> dict:
|
||||
"""Device status by entity key."""
|
||||
return cast(dict, self.wrapper.device.status[self.key])
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to HASS."""
|
||||
self.async_on_remove(self.wrapper.async_add_listener(self._update_callback))
|
||||
|
@ -58,6 +58,7 @@ MOCK_BLOCKS = [
|
||||
MOCK_CONFIG = {
|
||||
"input:0": {"id": 0, "type": "button"},
|
||||
"switch:0": {"name": "test switch_0"},
|
||||
"cover:0": {"name": "test cover_0"},
|
||||
"sys": {
|
||||
"ui_data": {},
|
||||
"device": {"name": "Test name"},
|
||||
@ -84,6 +85,7 @@ MOCK_STATUS_COAP = {
|
||||
|
||||
MOCK_STATUS_RPC = {
|
||||
"switch:0": {"output": True},
|
||||
"cover:0": {"state": "stopped", "pos_control": True, "current_pos": 50},
|
||||
"sys": {
|
||||
"available_updates": {
|
||||
"beta": {"version": "some_beta_version"},
|
||||
|
@ -12,13 +12,13 @@ from homeassistant.components.cover import (
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
|
||||
|
||||
ROLLER_BLOCK_ID = 1
|
||||
|
||||
|
||||
async def test_services(hass, coap_wrapper, monkeypatch):
|
||||
"""Test device turn on/off services."""
|
||||
async def test_block_device_services(hass, coap_wrapper, monkeypatch):
|
||||
"""Test block device cover services."""
|
||||
assert coap_wrapper
|
||||
|
||||
monkeypatch.setitem(coap_wrapper.device.settings, "mode", "roller")
|
||||
@ -61,8 +61,8 @@ async def test_services(hass, coap_wrapper, monkeypatch):
|
||||
assert hass.states.get("cover.test_name").state == STATE_CLOSED
|
||||
|
||||
|
||||
async def test_update(hass, coap_wrapper, monkeypatch):
|
||||
"""Test device update."""
|
||||
async def test_block_device_update(hass, coap_wrapper, monkeypatch):
|
||||
"""Test block device update."""
|
||||
assert coap_wrapper
|
||||
|
||||
hass.async_create_task(
|
||||
@ -81,8 +81,8 @@ async def test_update(hass, coap_wrapper, monkeypatch):
|
||||
assert hass.states.get("cover.test_name").state == STATE_OPEN
|
||||
|
||||
|
||||
async def test_no_roller_blocks(hass, coap_wrapper, monkeypatch):
|
||||
"""Test device without roller blocks."""
|
||||
async def test_block_device_no_roller_blocks(hass, coap_wrapper, monkeypatch):
|
||||
"""Test block device without roller blocks."""
|
||||
assert coap_wrapper
|
||||
|
||||
monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "type", None)
|
||||
@ -91,3 +91,101 @@ async def test_no_roller_blocks(hass, coap_wrapper, monkeypatch):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("cover.test_name") is None
|
||||
|
||||
|
||||
async def test_rpc_device_services(hass, rpc_wrapper, monkeypatch):
|
||||
"""Test RPC device cover services."""
|
||||
assert rpc_wrapper
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
{ATTR_ENTITY_ID: "cover.test_cover_0", ATTR_POSITION: 50},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get("cover.test_cover_0")
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 50
|
||||
|
||||
monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "opening")
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: "cover.test_cover_0"},
|
||||
blocking=True,
|
||||
)
|
||||
rpc_wrapper.async_set_updated_data("")
|
||||
assert hass.states.get("cover.test_cover_0").state == STATE_OPENING
|
||||
|
||||
monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closing")
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
{ATTR_ENTITY_ID: "cover.test_cover_0"},
|
||||
blocking=True,
|
||||
)
|
||||
rpc_wrapper.async_set_updated_data("")
|
||||
assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING
|
||||
|
||||
monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closed")
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_STOP_COVER,
|
||||
{ATTR_ENTITY_ID: "cover.test_cover_0"},
|
||||
blocking=True,
|
||||
)
|
||||
rpc_wrapper.async_set_updated_data("")
|
||||
assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED
|
||||
|
||||
|
||||
async def test_rpc_device_no_cover_keys(hass, rpc_wrapper, monkeypatch):
|
||||
"""Test RPC device without cover keys."""
|
||||
assert rpc_wrapper
|
||||
|
||||
monkeypatch.delitem(rpc_wrapper.device.status, "cover:0")
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("cover.test_cover_0") is None
|
||||
|
||||
|
||||
async def test_rpc_device_update(hass, rpc_wrapper, monkeypatch):
|
||||
"""Test RPC device update."""
|
||||
assert rpc_wrapper
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closed")
|
||||
await hass.helpers.entity_component.async_update_entity("cover.test_cover_0")
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED
|
||||
|
||||
monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "open")
|
||||
await hass.helpers.entity_component.async_update_entity("cover.test_cover_0")
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("cover.test_cover_0").state == STATE_OPEN
|
||||
|
||||
|
||||
async def test_rpc_device_no_position_control(hass, rpc_wrapper, monkeypatch):
|
||||
"""Test RPC device with no position control."""
|
||||
assert rpc_wrapper
|
||||
|
||||
monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "pos_control", False)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity("cover.test_cover_0")
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("cover.test_cover_0").state == STATE_UNKNOWN
|
||||
|
Loading…
x
Reference in New Issue
Block a user