mirror of https://github.com/home-assistant/core
Add zwave_mqtt light platform (#35337)
This commit is contained in:
parent
6146eaa350
commit
7e4aa2409f
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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."""
|
||||
|
|
|
@ -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"
|
|
@ -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
Loading…
Reference in New Issue