Add tilt support to Tasmota covers (#71789)

* Add tilt support to Tasmota covers

* Bump hatasmota to 0.5.0
This commit is contained in:
Erik Montnemery 2022-05-13 21:03:21 +02:00 committed by GitHub
parent 807df530bc
commit 08ee276277
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 157 additions and 33 deletions

View File

@ -9,6 +9,7 @@ from hatasmota.models import DiscoveryHashType
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
ATTR_TILT_POSITION,
DOMAIN as COVER_DOMAIN, DOMAIN as COVER_DOMAIN,
CoverEntity, CoverEntity,
CoverEntityFeature, CoverEntityFeature,
@ -55,23 +56,32 @@ class TasmotaCover(
): ):
"""Representation of a Tasmota cover.""" """Representation of a Tasmota cover."""
_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.STOP
| CoverEntityFeature.SET_POSITION
)
_tasmota_entity: tasmota_shutter.TasmotaShutter _tasmota_entity: tasmota_shutter.TasmotaShutter
def __init__(self, **kwds: Any) -> None: def __init__(self, **kwds: Any) -> None:
"""Initialize the Tasmota cover.""" """Initialize the Tasmota cover."""
self._direction: int | None = None self._direction: int | None = None
self._position: int | None = None self._position: int | None = None
self._tilt_position: int | None = None
super().__init__( super().__init__(
**kwds, **kwds,
) )
self._attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.STOP
| CoverEntityFeature.SET_POSITION
)
if self._tasmota_entity.supports_tilt:
self._attr_supported_features |= (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.STOP_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to MQTT events.""" """Subscribe to MQTT events."""
self._tasmota_entity.set_on_state_callback(self.cover_state_updated) self._tasmota_entity.set_on_state_callback(self.cover_state_updated)
@ -82,6 +92,7 @@ class TasmotaCover(
"""Handle state updates.""" """Handle state updates."""
self._direction = kwargs["direction"] self._direction = kwargs["direction"]
self._position = kwargs["position"] self._position = kwargs["position"]
self._tilt_position = kwargs["tilt"]
self.async_write_ha_state() self.async_write_ha_state()
@property @property
@ -92,6 +103,14 @@ class TasmotaCover(
""" """
return self._position return self._position
@property
def current_cover_tilt_position(self) -> int | None:
"""Return current tilt position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._tilt_position
@property @property
def is_opening(self) -> bool: def is_opening(self) -> bool:
"""Return if the cover is opening or not.""" """Return if the cover is opening or not."""
@ -125,3 +144,20 @@ class TasmotaCover(
async def async_stop_cover(self, **kwargs: Any) -> None: async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover.""" """Stop the cover."""
await self._tasmota_entity.stop() await self._tasmota_entity.stop()
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
await self._tasmota_entity.open_tilt()
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close cover tilt."""
await self._tasmota_entity.close_tilt()
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
tilt = kwargs[ATTR_TILT_POSITION]
await self._tasmota_entity.set_tilt_position(tilt)
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop the cover tilt."""
await self._tasmota_entity.stop()

View File

@ -3,7 +3,7 @@
"name": "Tasmota", "name": "Tasmota",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tasmota", "documentation": "https://www.home-assistant.io/integrations/tasmota",
"requirements": ["hatasmota==0.4.1"], "requirements": ["hatasmota==0.5.0"],
"dependencies": ["mqtt"], "dependencies": ["mqtt"],
"mqtt": ["tasmota/discovery/#"], "mqtt": ["tasmota/discovery/#"],
"codeowners": ["@emontnemery"], "codeowners": ["@emontnemery"],

View File

@ -792,7 +792,7 @@ hass-nabucasa==0.54.0
hass_splunk==0.1.1 hass_splunk==0.1.1
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.4.1 hatasmota==0.5.0
# homeassistant.components.jewish_calendar # homeassistant.components.jewish_calendar
hdate==0.10.4 hdate==0.10.4

View File

@ -565,7 +565,7 @@ hangups==0.4.18
hass-nabucasa==0.54.0 hass-nabucasa==0.54.0
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.4.1 hatasmota==0.5.0
# homeassistant.components.jewish_calendar # homeassistant.components.jewish_calendar
hdate==0.10.4 hdate==0.10.4

View File

@ -30,6 +30,19 @@ from .test_common import (
from tests.common import async_fire_mqtt_message from tests.common import async_fire_mqtt_message
COVER_SUPPORT = (
cover.SUPPORT_OPEN
| cover.SUPPORT_CLOSE
| cover.SUPPORT_STOP
| cover.SUPPORT_SET_POSITION
)
TILT_SUPPORT = (
cover.SUPPORT_OPEN_TILT
| cover.SUPPORT_CLOSE_TILT
| cover.SUPPORT_STOP_TILT
| cover.SUPPORT_SET_TILT_POSITION
)
async def test_missing_relay(hass, mqtt_mock, setup_tasmota): async def test_missing_relay(hass, mqtt_mock, setup_tasmota):
"""Test no cover is discovered if relays are missing.""" """Test no cover is discovered if relays are missing."""
@ -64,11 +77,46 @@ async def test_multiple_covers(
assert len(hass.states.async_all("cover")) == num_covers assert len(hass.states.async_all("cover")) == num_covers
async def test_tilt_support(hass, mqtt_mock, setup_tasmota):
"""Test tilt support detection."""
config = copy.deepcopy(DEFAULT_CONFIG)
config["rl"] = [3, 3, 3, 3, 3, 3, 3, 3]
config["sht"] = [
[0, 0, 0], # Default settings, no tilt
[-90, 90, 24], # Tilt configured
[-90, 90, 0], # Duration 0, no tilt
[-90, -90, 24], # min+max same, no tilt
]
mac = config["mac"]
async_fire_mqtt_message(
hass,
f"{DEFAULT_PREFIX}/{mac}/config",
json.dumps(config),
)
await hass.async_block_till_done()
assert len(hass.states.async_all("cover")) == 4
state = hass.states.get("cover.tasmota_cover_1")
assert state.attributes["supported_features"] == COVER_SUPPORT
state = hass.states.get("cover.tasmota_cover_2")
assert state.attributes["supported_features"] == COVER_SUPPORT | TILT_SUPPORT
state = hass.states.get("cover.tasmota_cover_3")
assert state.attributes["supported_features"] == COVER_SUPPORT
state = hass.states.get("cover.tasmota_cover_4")
assert state.attributes["supported_features"] == COVER_SUPPORT
async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
"""Test state update via MQTT.""" """Test state update via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG) config = copy.deepcopy(DEFAULT_CONFIG)
config["rl"][0] = 3 config["rl"][0] = 3
config["rl"][1] = 3 config["rl"][1] = 3
config["sht"] = [[-90, 90, 24]]
mac = config["mac"] mac = config["mac"]
async_fire_mqtt_message( async_fire_mqtt_message(
@ -86,40 +134,39 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
assert ( assert state.attributes["supported_features"] == COVER_SUPPORT | TILT_SUPPORT
state.attributes["supported_features"]
== cover.SUPPORT_OPEN
| cover.SUPPORT_CLOSE
| cover.SUPPORT_STOP
| cover.SUPPORT_SET_POSITION
)
assert not state.attributes.get(ATTR_ASSUMED_STATE) assert not state.attributes.get(ATTR_ASSUMED_STATE)
# Periodic updates # Periodic updates
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
"tasmota_49A3BC/tele/SENSOR", "tasmota_49A3BC/tele/SENSOR",
'{"Shutter1":{"Position":54,"Direction":-1}}', '{"Shutter1":{"Position":54,"Direction":-1,"Tilt":-90}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "closing" assert state.state == "closing"
assert state.attributes["current_position"] == 54 assert state.attributes["current_position"] == 54
assert state.attributes["current_tilt_position"] == 0
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
"tasmota_49A3BC/tele/SENSOR", "tasmota_49A3BC/tele/SENSOR",
'{"Shutter1":{"Position":100,"Direction":1}}', '{"Shutter1":{"Position":100,"Direction":1,"Tilt":90}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "opening" assert state.state == "opening"
assert state.attributes["current_position"] == 100 assert state.attributes["current_position"] == 100
assert state.attributes["current_tilt_position"] == 100
async_fire_mqtt_message( async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":0,"Direction":0}}' hass,
"tasmota_49A3BC/tele/SENSOR",
'{"Shutter1":{"Position":0,"Direction":0,"Tilt":0}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "closed" assert state.state == "closed"
assert state.attributes["current_position"] == 0 assert state.attributes["current_position"] == 0
assert state.attributes["current_tilt_position"] == 50
async_fire_mqtt_message( async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":1,"Direction":0}}' hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":1,"Direction":0}}'
@ -141,29 +188,32 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
"tasmota_49A3BC/stat/STATUS10", "tasmota_49A3BC/stat/STATUS10",
'{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1}}}', '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1,"Tilt":-90}}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "closing" assert state.state == "closing"
assert state.attributes["current_position"] == 54 assert state.attributes["current_position"] == 54
assert state.attributes["current_tilt_position"] == 0
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
"tasmota_49A3BC/stat/STATUS10", "tasmota_49A3BC/stat/STATUS10",
'{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1}}}', '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1,"Tilt":90}}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "opening" assert state.state == "opening"
assert state.attributes["current_position"] == 100 assert state.attributes["current_position"] == 100
assert state.attributes["current_tilt_position"] == 100
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
"tasmota_49A3BC/stat/STATUS10", "tasmota_49A3BC/stat/STATUS10",
'{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0}}}', '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0,"Tilt":0}}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "closed" assert state.state == "closed"
assert state.attributes["current_position"] == 0 assert state.attributes["current_position"] == 0
assert state.attributes["current_tilt_position"] == 50
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
@ -187,27 +237,32 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
"tasmota_49A3BC/stat/RESULT", "tasmota_49A3BC/stat/RESULT",
'{"Shutter1":{"Position":54,"Direction":-1}}', '{"Shutter1":{"Position":54,"Direction":-1,"Tilt":-90}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "closing" assert state.state == "closing"
assert state.attributes["current_position"] == 54 assert state.attributes["current_position"] == 54
assert state.attributes["current_tilt_position"] == 0
async_fire_mqtt_message( async_fire_mqtt_message(
hass, hass,
"tasmota_49A3BC/stat/RESULT", "tasmota_49A3BC/stat/RESULT",
'{"Shutter1":{"Position":100,"Direction":1}}', '{"Shutter1":{"Position":100,"Direction":1,"Tilt":90}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "opening" assert state.state == "opening"
assert state.attributes["current_position"] == 100 assert state.attributes["current_position"] == 100
assert state.attributes["current_tilt_position"] == 100
async_fire_mqtt_message( async_fire_mqtt_message(
hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":0,"Direction":0}}' hass,
"tasmota_49A3BC/stat/RESULT",
'{"Shutter1":{"Position":0,"Direction":0,"Tilt":0}}',
) )
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == "closed" assert state.state == "closed"
assert state.attributes["current_position"] == 0 assert state.attributes["current_position"] == 0
assert state.attributes["current_tilt_position"] == 50
async_fire_mqtt_message( async_fire_mqtt_message(
hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}' hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}'
@ -249,14 +304,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("cover.tasmota_cover_1") state = hass.states.get("cover.tasmota_cover_1")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
assert ( assert state.attributes["supported_features"] == COVER_SUPPORT
state.attributes["supported_features"]
== cover.SUPPORT_OPEN
| cover.SUPPORT_CLOSE
| cover.SUPPORT_STOP
| cover.SUPPORT_SET_POSITION
)
assert not state.attributes.get(ATTR_ASSUMED_STATE)
# Periodic updates # Periodic updates
async_fire_mqtt_message( async_fire_mqtt_message(
@ -405,6 +453,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
config["dn"] = "Test" config["dn"] = "Test"
config["rl"][0] = 3 config["rl"][0] = 3
config["rl"][1] = 3 config["rl"][1] = 3
config["sht"] = [[-90, 90, 24]]
mac = config["mac"] mac = config["mac"]
async_fire_mqtt_message( async_fire_mqtt_message(
@ -461,6 +510,45 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
) )
mqtt_mock.async_publish.reset_mock() mqtt_mock.async_publish.reset_mock()
# Close the cover tilt and verify MQTT message is sent
await call_service(hass, "cover.test_cover_1", "close_cover_tilt")
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/ShutterTilt1", "CLOSE", 0, False
)
mqtt_mock.async_publish.reset_mock()
# Open the cover tilt and verify MQTT message is sent
await call_service(hass, "cover.test_cover_1", "open_cover_tilt")
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/ShutterTilt1", "OPEN", 0, False
)
mqtt_mock.async_publish.reset_mock()
# Stop the cover tilt and verify MQTT message is sent
await call_service(hass, "cover.test_cover_1", "stop_cover_tilt")
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/ShutterStop1", "", 0, False
)
mqtt_mock.async_publish.reset_mock()
# Set tilt position and verify MQTT message is sent
await call_service(
hass, "cover.test_cover_1", "set_cover_tilt_position", tilt_position=0
)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/ShutterTilt1", "-90", 0, False
)
mqtt_mock.async_publish.reset_mock()
# Set tilt position and verify MQTT message is sent
await call_service(
hass, "cover.test_cover_1", "set_cover_tilt_position", tilt_position=100
)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/ShutterTilt1", "90", 0, False
)
mqtt_mock.async_publish.reset_mock()
async def test_sending_mqtt_commands_inverted(hass, mqtt_mock, setup_tasmota): async def test_sending_mqtt_commands_inverted(hass, mqtt_mock, setup_tasmota):
"""Test the sending MQTT commands.""" """Test the sending MQTT commands."""