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:
Michał Mrozek 2020-06-30 13:02:30 +02:00 committed by GitHub
parent 4d17b18761
commit 61475d0a0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 369 additions and 0 deletions

View file

@ -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,

View 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)

View file

@ -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,

View file

@ -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."""

View 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
View 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
}
}

File diff suppressed because one or more lines are too long