Add zwave_mqtt light platform (#35337)

This commit is contained in:
Martin Hjelmare 2020-05-07 23:52:54 +02:00 committed by GitHub
parent 6146eaa350
commit 7e4aa2409f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 423 additions and 3 deletions

View File

@ -107,6 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
@callback
def async_value_added(value):
node = value.node
# Clean up node.node_id and node.id use. They are the same.
node_id = value.node.node_id
# Filter out CommandClasses we're definitely not interested in.
@ -147,6 +148,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
values = ZWaveDeviceEntityValues(hass, options, schema, value)
values.async_setup()
# This is legacy and can be cleaned up since we are in the main thread:
# We create a new list and update the reference here so that
# the list can be safely iterated over in the main thread
data_values[node_id] = node_data_values + [values]

View File

@ -1,10 +1,11 @@
"""Constants for the zwave_mqtt integration."""
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
DOMAIN = "zwave_mqtt"
DATA_UNSUBSCRIBE = "unsubscribe"
PLATFORMS = [SENSOR_DOMAIN, SWITCH_DOMAIN]
PLATFORMS = [LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
# MQTT Topics
TOPIC_OPENZWAVE = "OpenZWave"

View File

@ -1,10 +1,44 @@
"""Map Z-Wave nodes and values to Home Assistant entities."""
import openzwavemqtt.const as const_ozw
from openzwavemqtt.const import CommandClass, ValueGenre, ValueType
from openzwavemqtt.const import CommandClass, ValueGenre, ValueIndex, ValueType
from . import const
DISCOVERY_SCHEMAS = (
{ # Light
const.DISC_COMPONENT: "light",
const.DISC_GENERIC_DEVICE_CLASS: (
const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,
const_ozw.GENERIC_TYPE_SWITCH_REMOTE,
),
const.DISC_SPECIFIC_DEVICE_CLASS: (
const_ozw.SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL,
const_ozw.SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL,
const_ozw.SPECIFIC_TYPE_NOT_USED,
),
const.DISC_VALUES: {
const.DISC_PRIMARY: {
const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,),
const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL,
const.DISC_TYPE: ValueType.BYTE,
},
"dimming_duration": {
const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,),
const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DURATION,
const.DISC_OPTIONAL: True,
},
"color": {
const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_COLOR,),
const.DISC_INDEX: ValueIndex.SWITCH_COLOR_COLOR,
const.DISC_OPTIONAL: True,
},
"color_channels": {
const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_COLOR,),
const.DISC_INDEX: ValueIndex.SWITCH_COLOR_CHANNELS,
const.DISC_OPTIONAL: True,
},
},
},
{ # All other text/numeric sensors
const.DISC_COMPONENT: "sensor",
const.DISC_VALUES: {

View File

@ -40,7 +40,7 @@ class ZWaveDeviceEntityValues:
# and add a check to the schema to make sure the Instance matches.
for name, disc_settings in self._schema[const.DISC_VALUES].items():
self._values[name] = None
disc_settings[const.DISC_INSTANCE] = [primary_value.instance]
disc_settings[const.DISC_INSTANCE] = (primary_value.instance,)
self._values[const.DISC_PRIMARY] = primary_value
self._node = primary_value.node

View File

@ -0,0 +1,135 @@
"""Support for Z-Wave lights."""
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_TRANSITION,
DOMAIN as LIGHT_DOMAIN,
SUPPORT_BRIGHTNESS,
SUPPORT_TRANSITION,
LightEntity,
)
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__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Z-Wave Light from Config Entry."""
@callback
def async_add_light(values):
"""Add Z-Wave Light."""
light = ZwaveDimmer(values)
async_add_entities([light])
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
async_dispatcher_connect(hass, f"{DOMAIN}_new_{LIGHT_DOMAIN}", async_add_light)
)
def byte_to_zwave_brightness(value):
"""Convert brightness in 0-255 scale to 0-99 scale.
`value` -- (int) Brightness byte value from 0-255.
"""
if value > 0:
return max(1, round((value / 255) * 99))
return 0
class ZwaveDimmer(ZWaveDeviceEntity, LightEntity):
"""Representation of a Z-Wave dimmer."""
def __init__(self, values):
"""Initialize the light."""
super().__init__(values)
self._supported_features = SUPPORT_BRIGHTNESS
# make sure that supported features is correctly set
self.on_value_update()
@callback
def on_value_update(self):
"""Call when the underlying value(s) is added or updated."""
self._supported_features = SUPPORT_BRIGHTNESS
if self.values.dimming_duration is not None:
self._supported_features |= SUPPORT_TRANSITION
@property
def brightness(self):
"""Return the brightness of this light between 0..255.
Zwave multilevel switches use a range of [0, 99] to control brightness.
"""
if "target" in self.values:
return round((self.values.target.value / 99) * 255)
return round((self.values.primary.value / 99) * 255)
@property
def is_on(self):
"""Return true if device is on (brightness above 0)."""
if "target" in self.values:
return self.values.target.value > 0
return self.values.primary.value > 0
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
@callback
def async_set_duration(self, **kwargs):
"""Set the transition time for the brightness value.
Zwave Dimming Duration values:
0 = instant
0-127 = 1 second to 127 seconds
128-254 = 1 minute to 127 minutes
255 = factory default
"""
if self.values.dimming_duration is None:
return
if ATTR_TRANSITION not in kwargs:
# no transition specified by user, use defaults
new_value = 255
else:
# transition specified by user, convert to zwave value
transition = kwargs[ATTR_TRANSITION]
if transition <= 127:
new_value = int(transition)
else:
minutes = int(transition / 60)
_LOGGER.debug(
"Transition rounded to %d minutes for %s", minutes, self.entity_id
)
new_value = minutes + 128
# only send value if it differs from current
# this prevents a command for nothing
if self.values.dimming_duration.value != new_value:
self.values.dimming_duration.send_value(new_value)
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
self.async_set_duration(**kwargs)
# Zwave multilevel switches use a range of [0, 99] to control
# brightness. Level 255 means to set it to previous value.
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
brightness = byte_to_zwave_brightness(brightness)
else:
brightness = 255
self.values.primary.send_value(brightness)
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
self.async_set_duration(**kwargs)
self.values.primary.send_value(0)

View File

@ -15,6 +15,12 @@ def generic_data_fixture():
return load_fixture(f"zwave_mqtt/generic_network_dump.csv")
@pytest.fixture(name="light_data", scope="session")
def light_data_fixture():
"""Load light dimmer MQTT data and return it."""
return load_fixture(f"zwave_mqtt/light_network_dump.csv")
@pytest.fixture(name="sent_messages")
def sent_messages_fixture():
"""Fixture to capture sent messages."""
@ -29,6 +35,17 @@ def sent_messages_fixture():
yield sent_messages
@pytest.fixture(name="light_msg")
async def light_msg_fixture(hass):
"""Return a mock MQTT msg with a light actuator message."""
light_json = json.loads(
await hass.async_add_executor_job(load_fixture, "zwave_mqtt/light.json")
)
message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"])
message.encode()
return message
@pytest.fixture(name="switch_msg")
async def switch_msg_fixture(hass):
"""Return a mock MQTT msg with a switch actuator message."""

View File

@ -0,0 +1,151 @@
"""Test Z-Wave Lights."""
from homeassistant.components.zwave_mqtt.light import byte_to_zwave_brightness
from .common import setup_zwave
async def test_light(hass, light_data, light_msg, sent_messages):
"""Test setting up config entry."""
receive_message = await setup_zwave(hass, fixture=light_data)
# Test loaded
state = hass.states.get("light.led_bulb_6_multi_colour_level")
assert state is not None
assert state.state == "off"
# Test turning on
# Beware that due to rounding, a roundtrip conversion does not always work
new_brightness = 44
new_transition = 0
await hass.services.async_call(
"light",
"turn_on",
{
"entity_id": "light.led_bulb_6_multi_colour_level",
"brightness": new_brightness,
"transition": new_transition,
},
blocking=True,
)
assert len(sent_messages) == 2
msg = sent_messages[0]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {"Value": 0, "ValueIDKey": 1407375551070225}
msg = sent_messages[1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {
"Value": byte_to_zwave_brightness(new_brightness),
"ValueIDKey": 659128337,
}
# Feedback on state
light_msg.decode()
light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness)
light_msg.encode()
receive_message(light_msg)
await hass.async_block_till_done()
state = hass.states.get("light.led_bulb_6_multi_colour_level")
assert state is not None
assert state.state == "on"
assert state.attributes["brightness"] == new_brightness
# Test turning off
new_transition = 6553
await hass.services.async_call(
"light",
"turn_off",
{
"entity_id": "light.led_bulb_6_multi_colour_level",
"transition": new_transition,
},
blocking=True,
)
assert len(sent_messages) == 4
msg = sent_messages[-2]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {"Value": 237, "ValueIDKey": 1407375551070225}
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {"Value": 0, "ValueIDKey": 659128337}
# Feedback on state
light_msg.decode()
light_msg.payload["Value"] = 0
light_msg.encode()
receive_message(light_msg)
await hass.async_block_till_done()
state = hass.states.get("light.led_bulb_6_multi_colour_level")
assert state is not None
assert state.state == "off"
# Test turn on without brightness
new_transition = 127
await hass.services.async_call(
"light",
"turn_on",
{
"entity_id": "light.led_bulb_6_multi_colour_level",
"transition": new_transition,
},
blocking=True,
)
assert len(sent_messages) == 6
msg = sent_messages[-2]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {"Value": 127, "ValueIDKey": 1407375551070225}
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {
"Value": 255,
"ValueIDKey": 659128337,
}
# Feedback on state
light_msg.decode()
light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness)
light_msg.encode()
receive_message(light_msg)
await hass.async_block_till_done()
state = hass.states.get("light.led_bulb_6_multi_colour_level")
assert state is not None
assert state.state == "on"
assert state.attributes["brightness"] == new_brightness
# Test set brightness to 0
new_brightness = 0
await hass.services.async_call(
"light",
"turn_on",
{
"entity_id": "light.led_bulb_6_multi_colour_level",
"brightness": new_brightness,
},
blocking=True,
)
assert len(sent_messages) == 7
msg = sent_messages[-1]
assert msg["topic"] == "OpenZWave/1/command/setvalue/"
assert msg["payload"] == {
"Value": byte_to_zwave_brightness(new_brightness),
"ValueIDKey": 659128337,
}
# Feedback on state
light_msg.decode()
light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness)
light_msg.encode()
receive_message(light_msg)
await hass.async_block_till_done()
state = hass.states.get("light.led_bulb_6_multi_colour_level")
assert state is not None
assert state.state == "off"

25
tests/fixtures/zwave_mqtt/light.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
"topic": "OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/",
"payload": {
"Label": "Level",
"Value": 0,
"Units": "",
"Min": 0,
"Max": 255,
"Type": "Byte",
"Instance": 1,
"CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL",
"Index": 0,
"Node": 39,
"Genre": "User",
"Help": "The Current Level of the Device",
"ValueIDKey": 659128337,
"ReadOnly": false,
"WriteOnly": false,
"ValueSet": false,
"ValuePolled": false,
"ChangeVerified": false,
"Event": "valueAdded",
"TimeStamp": 1579566891
}
}

File diff suppressed because one or more lines are too long