Skip to content

Commit

Permalink
Support for zwave light transitions (#6868)
Browse files Browse the repository at this point in the history
* Support for zwave light transitions

* Dimming duration is optional

* Updated supported_features to show transition
  • Loading branch information
emlove committed Apr 3, 2017
1 parent 01e581a commit 06e1c21
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 27 deletions.
84 changes: 61 additions & 23 deletions homeassistant/components/light/zwave.py
Expand Up @@ -10,8 +10,8 @@
# pylint: disable=import-error
from threading import Timer
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
SUPPORT_RGB_COLOR, DOMAIN, Light
ATTR_RGB_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, DOMAIN, Light
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import STATE_OFF, STATE_ON
Expand Down Expand Up @@ -43,11 +43,6 @@
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN

SUPPORT_ZWAVE_DIMMER = SUPPORT_BRIGHTNESS
SUPPORT_ZWAVE_COLOR = SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
| SUPPORT_COLOR_TEMP)


def get_device(node, values, node_config, **kwargs):
"""Create zwave entity device."""
Expand All @@ -72,6 +67,13 @@ def brightness_state(value):
return 0, STATE_OFF


def ct_to_rgb(temp):
"""Convert color temperature (mireds) to RGB."""
colorlist = list(
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
return [int(val) for val in colorlist]


class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
"""Representation of a Z-Wave dimmer."""

Expand All @@ -80,6 +82,7 @@ def __init__(self, values, refresh, delay):
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
self._brightness = None
self._state = None
self._supported_features = None
self._delay = delay
self._refresh_value = refresh
self._zw098 = None
Expand All @@ -100,13 +103,20 @@ def __init__(self, values, refresh, delay):
self._timer = None
_LOGGER.debug('self._refreshing=%s self.delay=%s',
self._refresh_value, self._delay)
self.value_added()
self.update_properties()

def update_properties(self):
"""Update internal properties based on zwave values."""
# Brightness
self._brightness, self._state = brightness_state(self.values.primary)

def value_added(self):
"""Called when a new value is added to this entity."""
self._supported_features = SUPPORT_BRIGHTNESS
if self.values.dimming_duration is not None:
self._supported_features |= SUPPORT_TRANSITION

def value_changed(self):
"""Called when a value for this entity's node has changed."""
if self._refresh_value:
Expand Down Expand Up @@ -139,10 +149,43 @@ def is_on(self):
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_ZWAVE_DIMMER
return self._supported_features

def _set_duration(self, **kwargs):
"""Set the transition time for the brightness value.
Zwave Dimming Duration values:
0x00 = instant
0x01-0x7F = 1 second to 127 seconds
0x80-0xFE = 1 minute to 127 minutes
0xFF = factory default
"""
if self.values.dimming_duration is None:
if ATTR_TRANSITION in kwargs:
_LOGGER.debug("Dimming not supported by %s.", self.entity_id)
return

if ATTR_TRANSITION not in kwargs:
self.values.dimming_duration.data = 0xFF
return

transition = kwargs[ATTR_TRANSITION]
if transition <= 127:
self.values.dimming_duration.data = int(transition)
elif transition > 7620:
self.values.dimming_duration.data = 0xFE
_LOGGER.warning("Transition clipped to 127 minutes for %s.",
self.entity_id)
else:
minutes = int(transition / 60)
_LOGGER.debug("Transition rounded to %d minutes for %s.",
minutes, self.entity_id)
self.values.dimming_duration.data = minutes + 0x7F

def turn_on(self, **kwargs):
"""Turn the device on."""
self._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:
Expand All @@ -156,17 +199,12 @@ def turn_on(self, **kwargs):

def turn_off(self, **kwargs):
"""Turn the device off."""
self._set_duration(**kwargs)

if self.node.set_dimmer(self.values.primary.value_id, 0):
self._state = STATE_OFF


def ct_to_rgb(temp):
"""Convert color temperature (mireds) to RGB."""
colorlist = list(
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
return [int(val) for val in colorlist]


class ZwaveColorLight(ZwaveDimmer):
"""Representation of a Z-Wave color changing light."""

Expand All @@ -178,6 +216,14 @@ def __init__(self, values, refresh, delay):

super().__init__(values, refresh, delay)

def value_added(self):
"""Called when a new value is added to this entity."""
super().value_added()

self._supported_features |= SUPPORT_RGB_COLOR
if self._zw098:
self._supported_features |= SUPPORT_COLOR_TEMP

def update_properties(self):
"""Update internal properties based on zwave values."""
super().update_properties()
Expand Down Expand Up @@ -288,11 +334,3 @@ def turn_on(self, **kwargs):
self.values.color.data = rgbw

super().turn_on(**kwargs)

@property
def supported_features(self):
"""Flag supported features."""
if self._zw098:
return SUPPORT_ZWAVE_COLORTEMP
else:
return SUPPORT_ZWAVE_COLOR
5 changes: 5 additions & 0 deletions homeassistant/components/zwave/__init__.py
Expand Up @@ -669,6 +669,7 @@ def check_value(self, value):
continue
self._values[name] = value
if self._entity:
self._entity.value_added()
self._entity.value_changed()

self._check_entity_ready()
Expand Down Expand Up @@ -778,6 +779,10 @@ def network_value_changed(self, value):
if value.value_id in [v.value_id for v in self.values if v]:
return self.value_changed()

def value_added(self):
"""Called when a new value is added to this entity."""
pass

def value_changed(self):
"""Called when a value for this entity's node has changed."""
self._update_attributes()
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/zwave/discovery_schemas.py
Expand Up @@ -121,6 +121,13 @@
const.DISC_GENRE: const.GENRE_USER,
const.DISC_TYPE: const.TYPE_BYTE,
},
'dimming_duration': {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL],
const.DISC_GENRE: const.GENRE_SYSTEM,
const.DISC_TYPE: const.TYPE_BYTE,
const.DISC_LABEL: 'Dimming Duration',
const.DISC_OPTIONAL: True,
},
'color': {
const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_COLOR],
const.DISC_GENRE: const.GENRE_USER,
Expand Down
63 changes: 59 additions & 4 deletions tests/components/light/test_zwave.py
Expand Up @@ -4,7 +4,9 @@
import homeassistant.components.zwave
from homeassistant.components.zwave import const
from homeassistant.components.light import (
zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR)
zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_RGB_COLOR,
SUPPORT_COLOR_TEMP)

from tests.mock.zwave import (
MockNode, MockValue, MockEntityValues, value_changed)
Expand All @@ -15,6 +17,7 @@ class MockLightValues(MockEntityValues):

def __init__(self, **kwargs):
"""Initialize the mock zwave values."""
self.dimming_duration = None
self.color = None
self.color_channels = None
super().__init__(**kwargs)
Expand All @@ -28,7 +31,7 @@ def test_get_device_detects_dimmer(mock_openzwave):

device = zwave.get_device(node=node, values=values, node_config={})
assert isinstance(device, zwave.ZwaveDimmer)
assert device.supported_features == zwave.SUPPORT_ZWAVE_DIMMER
assert device.supported_features == SUPPORT_BRIGHTNESS


def test_get_device_detects_colorlight(mock_openzwave):
Expand All @@ -39,7 +42,7 @@ def test_get_device_detects_colorlight(mock_openzwave):

device = zwave.get_device(node=node, values=values, node_config={})
assert isinstance(device, zwave.ZwaveColorLight)
assert device.supported_features == zwave.SUPPORT_ZWAVE_COLOR
assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR


def test_get_device_detects_zw098(mock_openzwave):
Expand All @@ -50,7 +53,8 @@ def test_get_device_detects_zw098(mock_openzwave):
values = MockLightValues(primary=value)
device = zwave.get_device(node=node, values=values, node_config={})
assert isinstance(device, zwave.ZwaveColorLight)
assert device.supported_features == zwave.SUPPORT_ZWAVE_COLORTEMP
assert device.supported_features == (
SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_COLOR_TEMP)


def test_dimmer_turn_on(mock_openzwave):
Expand All @@ -77,6 +81,57 @@ def test_dimmer_turn_on(mock_openzwave):
assert value_id == value.value_id
assert brightness == 46 # int(120 / 255 * 99)

with patch.object(zwave, '_LOGGER', MagicMock()) as mock_logger:
device.turn_on(**{ATTR_TRANSITION: 35})
assert mock_logger.debug.called
assert node.set_dimmer.called
msg, entity_id = mock_logger.debug.mock_calls[0][1]
assert entity_id == device.entity_id


def test_dimmer_transitions(mock_openzwave):
"""Test dimming transition on a dimmable Z-Wave light."""
node = MockNode()
value = MockValue(data=0, node=node)
duration = MockValue(data=0, node=node)
values = MockLightValues(primary=value, dimming_duration=duration)
device = zwave.get_device(node=node, values=values, node_config={})
assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION

# Test turn_on
# Factory Default
device.turn_on()
assert duration.data == 0xFF

# Seconds transition
device.turn_on(**{ATTR_TRANSITION: 45})
assert duration.data == 45

# Minutes transition
device.turn_on(**{ATTR_TRANSITION: 245})
assert duration.data == 0x83

# Clipped transition
device.turn_on(**{ATTR_TRANSITION: 10000})
assert duration.data == 0xFE

# Test turn_off
# Factory Default
device.turn_off()
assert duration.data == 0xFF

# Seconds transition
device.turn_off(**{ATTR_TRANSITION: 45})
assert duration.data == 45

# Minutes transition
device.turn_off(**{ATTR_TRANSITION: 245})
assert duration.data == 0x83

# Clipped transition
device.turn_off(**{ATTR_TRANSITION: 10000})
assert duration.data == 0xFE


def test_dimmer_turn_off(mock_openzwave):
"""Test turning off a dimmable Z-Wave light."""
Expand Down

0 comments on commit 06e1c21

Please sign in to comment.