Add support for window covers to ozw integration (#37217)
* feat: add cover to ozw * fix: imports * fix: formatting * fix: improve code regarding comments * add: cover tests * fix: add position converting tests * fix: convert cover position form zwave value * fix: improve naming * fix: increase coverage
This commit is contained in:
parent
4d17b18761
commit
61475d0a0c
7 changed files with 369 additions and 0 deletions
|
@ -1,6 +1,7 @@
|
|||
"""Constants for the ozw integration."""
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
||||
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||
|
@ -11,6 +12,7 @@ DOMAIN = "ozw"
|
|||
DATA_UNSUBSCRIBE = "unsubscribe"
|
||||
PLATFORMS = [
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
COVER_DOMAIN,
|
||||
CLIMATE_DOMAIN,
|
||||
FAN_DOMAIN,
|
||||
LIGHT_DOMAIN,
|
||||
|
|
76
homeassistant/components/ozw/cover.py
Normal file
76
homeassistant/components/ozw/cover.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
"""Support for Z-Wave cover devices."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_SET_POSITION,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import DATA_UNSUBSCRIBE, DOMAIN
|
||||
from .entity import ZWaveDeviceEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SUPPORTED_FEATURES_POSITION = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Z-Wave Cover from Config Entry."""
|
||||
|
||||
@callback
|
||||
def async_add_cover(values):
|
||||
"""Add Z-Wave Cover."""
|
||||
|
||||
async_add_entities([ZWaveCoverEntity(values)])
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
|
||||
async_dispatcher_connect(hass, f"{DOMAIN}_new_{COVER_DOMAIN}", async_add_cover)
|
||||
)
|
||||
|
||||
|
||||
def percent_to_zwave_position(value):
|
||||
"""Convert position in 0-100 scale to 0-99 scale.
|
||||
|
||||
`value` -- (int) Position byte value from 0-100.
|
||||
"""
|
||||
if value > 0:
|
||||
return max(1, round((value / 100) * 99))
|
||||
return 0
|
||||
|
||||
|
||||
class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity):
|
||||
"""Representation of a Z-Wave Cover device."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORTED_FEATURES_POSITION
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if cover is closed."""
|
||||
return self.values.primary.value == 0
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of cover where 0 means closed and 100 is fully open."""
|
||||
return round((self.values.primary.value / 99) * 100)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
self.values.primary.send_value(percent_to_zwave_position(kwargs[ATTR_POSITION]))
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self.values.primary.send_value(99)
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close cover."""
|
||||
self.values.primary.send_value(0)
|
|
@ -131,6 +131,35 @@ DISCOVERY_SCHEMAS = (
|
|||
},
|
||||
},
|
||||
},
|
||||
{ # Rollershutter
|
||||
const.DISC_COMPONENT: "cover",
|
||||
const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,),
|
||||
const.DISC_SPECIFIC_DEVICE_CLASS: (
|
||||
const_ozw.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL,
|
||||
const_ozw.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL,
|
||||
const_ozw.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL,
|
||||
const_ozw.SPECIFIC_TYPE_MOTOR_MULTIPOSITION,
|
||||
const_ozw.SPECIFIC_TYPE_SECURE_BARRIER_ADDON,
|
||||
const_ozw.SPECIFIC_TYPE_SECURE_DOOR,
|
||||
),
|
||||
const.DISC_VALUES: {
|
||||
const.DISC_PRIMARY: {
|
||||
const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL,
|
||||
const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL,
|
||||
const.DISC_GENRE: ValueGenre.USER,
|
||||
},
|
||||
"open": {
|
||||
const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL,
|
||||
const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_BRIGHT,
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
"close": {
|
||||
const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL,
|
||||
const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DIM,
|
||||
const.DISC_OPTIONAL: True,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ # Fan
|
||||
const.DISC_COMPONENT: "fan",
|
||||
const.DISC_GENERIC_DEVICE_CLASS: const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,
|
||||
|
|
|
@ -27,6 +27,12 @@ def light_data_fixture():
|
|||
return load_fixture("ozw/light_network_dump.csv")
|
||||
|
||||
|
||||
@pytest.fixture(name="cover_data", scope="session")
|
||||
def cover_data_fixture():
|
||||
"""Load cover MQTT data and return it."""
|
||||
return load_fixture("ozw/cover_network_dump.csv")
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_data", scope="session")
|
||||
def climate_data_fixture():
|
||||
"""Load climate MQTT data and return it."""
|
||||
|
@ -119,6 +125,17 @@ async def binary_sensor_alt_msg_fixture(hass):
|
|||
return message
|
||||
|
||||
|
||||
@pytest.fixture(name="cover_msg")
|
||||
async def cover_msg_fixture(hass):
|
||||
"""Return a mock MQTT msg with a cover level change message."""
|
||||
sensor_json = json.loads(
|
||||
await hass.async_add_executor_job(load_fixture, "ozw/cover.json")
|
||||
)
|
||||
message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"])
|
||||
message.encode()
|
||||
return message
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_msg")
|
||||
async def climate_msg_fixture(hass):
|
||||
"""Return a mock MQTT msg with a climate mode change message."""
|
||||
|
|
86
tests/components/ozw/test_cover.py
Normal file
86
tests/components/ozw/test_cover.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
"""Test Z-Wave Covers."""
|
||||
from homeassistant.components.cover import ATTR_CURRENT_POSITION
|
||||
|
||||
from .common import setup_ozw
|
||||
|
||||
|
||||
async def test_cover(hass, cover_data, sent_messages, cover_msg):
|
||||
"""Test setting up config entry."""
|
||||
receive_message = await setup_ozw(hass, fixture=cover_data)
|
||||
# Test loaded
|
||||
state = hass.states.get("cover.roller_shutter_3_instance_1_level")
|
||||
assert state is not None
|
||||
assert state.state == "closed"
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 0
|
||||
|
||||
# Test opening
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"open_cover",
|
||||
{"entity_id": "cover.roller_shutter_3_instance_1_level"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 1
|
||||
msg = sent_messages[0]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905}
|
||||
|
||||
# Feedback on state
|
||||
cover_msg.decode()
|
||||
cover_msg.payload["Value"] = 99
|
||||
cover_msg.encode()
|
||||
receive_message(cover_msg)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("cover.roller_shutter_3_instance_1_level")
|
||||
assert state is not None
|
||||
assert state.state == "open"
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 100
|
||||
|
||||
# Test closing
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"close_cover",
|
||||
{"entity_id": "cover.roller_shutter_3_instance_1_level"},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 2
|
||||
msg = sent_messages[1]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905}
|
||||
|
||||
# Test setting position
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"set_cover_position",
|
||||
{"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 50},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 3
|
||||
msg = sent_messages[2]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": 50, "ValueIDKey": 625573905}
|
||||
|
||||
# Test converting position to zwave range for position > 0
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"set_cover_position",
|
||||
{"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 100},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 4
|
||||
msg = sent_messages[3]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905}
|
||||
|
||||
# Test converting position to zwave range for position = 0
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"set_cover_position",
|
||||
{"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 0},
|
||||
blocking=True,
|
||||
)
|
||||
assert len(sent_messages) == 5
|
||||
msg = sent_messages[4]
|
||||
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
|
||||
assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905}
|
25
tests/fixtures/ozw/cover.json
vendored
Normal file
25
tests/fixtures/ozw/cover.json
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"topic": "OpenZWave/1/node/37/instance/1/commandclass/38/value/625573905/",
|
||||
"payload": {
|
||||
"Label": "Instance 1: Level",
|
||||
"Value": 0,
|
||||
"Units": "",
|
||||
"ValueSet": true,
|
||||
"ValuePolled": false,
|
||||
"ChangeVerified": false,
|
||||
"Min": 0,
|
||||
"Max": 255,
|
||||
"Type": "Byte",
|
||||
"Instance": 1,
|
||||
"CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL",
|
||||
"Index": 0,
|
||||
"Node": 37,
|
||||
"Genre": "User",
|
||||
"Help": "The Current Level of the Device",
|
||||
"ValueIDKey": 625573905,
|
||||
"ReadOnly": false,
|
||||
"WriteOnly": false,
|
||||
"Event": "valueChanged",
|
||||
"TimeStamp": 1593370642
|
||||
}
|
||||
}
|
134
tests/fixtures/ozw/cover_network_dump.csv
vendored
Normal file
134
tests/fixtures/ozw/cover_network_dump.csv
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue