Skip to content

Commit

Permalink
Refactor async_turn_on() for ZHA Light. (home-assistant#21156)
Browse files Browse the repository at this point in the history
* Refactor async_turn_on() for ZHA Light.

Use "move_to_level_with_on_off" if brightness or transition attributes
are present in the service call data, otherwise issue "On" Zigbee
command.
Allow brightness of 0 for service call -- effectively turning the light
off.
Send color commands only after the light was turned on.

* Fix zha.light tests.
  • Loading branch information
Adminiuga authored and dmulcahey committed Feb 27, 2019
1 parent 27e6c66 commit 9066609
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 50 deletions.
58 changes: 33 additions & 25 deletions homeassistant/components/zha/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

DEPENDENCIES = ['zha']

DEFAULT_DURATION = 0.5
DEFAULT_DURATION = 5

CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
Expand Down Expand Up @@ -110,8 +110,13 @@ def device_state_attributes(self):
return self.state_attributes

def set_level(self, value):
"""Set the brightness of this light between 0..255."""
value = max(0, min(255, value))
"""Set the brightness of this light between 0..254.
brightness level 255 is a special value instructing the device to come
on at `on_level` Zigbee attribute value, regardless of the last set
level
"""
value = max(0, min(254, value))
self._brightness = value
self.async_schedule_update_ha_state()

Expand Down Expand Up @@ -146,8 +151,31 @@ async def async_added_to_hass(self):

async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
duration = duration * 10 # tenths of s
transition = kwargs.get(light.ATTR_TRANSITION)
duration = transition * 10 if transition else DEFAULT_DURATION
brightness = kwargs.get(light.ATTR_BRIGHTNESS)

if (brightness is not None or transition) and \
self._supported_features & light.SUPPORT_BRIGHTNESS:
if brightness is not None:
level = min(254, brightness)
else:
level = self._brightness or 254
success = await self._level_channel.move_to_level_with_on_off(
level,
duration
)
if not success:
return
self._state = bool(level)
if level:
self._brightness = level

if brightness is None or brightness:
success = await self._on_off_channel.on()
if not success:
return
self._state = True

if light.ATTR_COLOR_TEMP in kwargs and \
self.supported_features & light.SUPPORT_COLOR_TEMP:
Expand All @@ -171,32 +199,12 @@ async def async_turn_on(self, **kwargs):
return
self._hs_color = hs_color

if self._brightness is not None:
brightness = kwargs.get(
light.ATTR_BRIGHTNESS, self._brightness or 255)
success = await self._level_channel.move_to_level_with_on_off(
brightness,
duration
)
if not success:
return
self._state = True
self._brightness = brightness
self.async_schedule_update_ha_state()
return

success = await self._on_off_channel.on()
if not success:
return

self._state = True
self.async_schedule_update_ha_state()

async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
duration = kwargs.get(light.ATTR_TRANSITION)
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
success = None
if duration and supports_level:
success = await self._level_channel.move_to_level_with_on_off(
0,
Expand Down
85 changes: 60 additions & 25 deletions tests/components/zha/test_light.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
"""Test zha light."""
from unittest.mock import call, patch
import asyncio
from unittest.mock import MagicMock, call, patch, sentinel

from homeassistant.components.light import DOMAIN
from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE
from tests.common import mock_coro
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE

from .common import (
async_init_zigpy_device, make_attribute, make_entity_id,
async_test_device_join, async_enable_traffic
)
async_enable_traffic, async_init_zigpy_device, async_test_device_join,
make_attribute, make_entity_id)

from tests.common import mock_coro

ON = 1
OFF = 0


async def test_light(hass, config_entry, zha_gateway):
async def test_light(hass, config_entry, zha_gateway, monkeypatch):
"""Test zha light platform."""
from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic
from zigpy.zcl.foundation import Status
from zigpy.profiles.zha import DeviceType

# create zigpy devices
Expand Down Expand Up @@ -52,6 +56,12 @@ async def test_light(hass, config_entry, zha_gateway):
# dimmable light
level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off
level_device_level_cluster = zigpy_device_level.endpoints.get(1).level
on_off_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock(
return_value=(sentinel.data, Status.SUCCESS))))
level_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock(
return_value=(sentinel.data, Status.SUCCESS))))
monkeypatch.setattr(level_device_on_off_cluster, 'request', on_off_mock)
monkeypatch.setattr(level_device_level_cluster, 'request', level_mock)
level_entity_id = make_entity_id(DOMAIN, zigpy_device_level,
level_device_on_off_cluster,
use_suffix=False)
Expand Down Expand Up @@ -81,7 +91,8 @@ async def test_light(hass, config_entry, zha_gateway):
hass, on_off_device_on_off_cluster, on_off_entity_id)

await async_test_level_on_off_from_hass(
hass, level_device_on_off_cluster, level_entity_id)
hass, level_device_on_off_cluster, level_device_level_cluster,
level_entity_id)

# test turning the lights on and off from the light
await async_test_on_from_light(
Expand Down Expand Up @@ -131,7 +142,7 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id):
await hass.services.async_call(DOMAIN, 'turn_on', {
'entity_id': entity_id
}, blocking=True)
assert len(cluster.request.mock_calls) == 1
assert cluster.request.call_count == 1
assert cluster.request.call_args == call(
False, ON, (), expect_reply=True, manufacturer=None)

Expand All @@ -148,28 +159,52 @@ async def async_test_off_from_hass(hass, cluster, entity_id):
await hass.services.async_call(DOMAIN, 'turn_off', {
'entity_id': entity_id
}, blocking=True)
assert len(cluster.request.mock_calls) == 1
assert cluster.request.call_count == 1
assert cluster.request.call_args == call(
False, OFF, (), expect_reply=True, manufacturer=None)


async def async_test_level_on_off_from_hass(hass, cluster, entity_id):
async def async_test_level_on_off_from_hass(hass, on_off_cluster,
level_cluster, entity_id):
"""Test on off functionality from hass."""
from zigpy import types
from zigpy.zcl.foundation import Status
with patch(
'zigpy.zcl.Cluster.request',
return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
# turn on via UI
await hass.services.async_call(DOMAIN, 'turn_on', {
'entity_id': entity_id
}, blocking=True)
assert len(cluster.request.mock_calls) == 1
assert cluster.request.call_args == call(
False, 4, (types.uint8_t, types.uint16_t), 255, 5.0,
expect_reply=True, manufacturer=None)

await async_test_off_from_hass(hass, cluster, entity_id)
# turn on via UI
await hass.services.async_call(DOMAIN, 'turn_on', {'entity_id': entity_id},
blocking=True)
assert on_off_cluster.request.call_count == 1
assert level_cluster.request.call_count == 0
assert on_off_cluster.request.call_args == call(
False, 1, (), expect_reply=True, manufacturer=None)
on_off_cluster.request.reset_mock()
level_cluster.request.reset_mock()

await hass.services.async_call(DOMAIN, 'turn_on',
{'entity_id': entity_id, 'transition': 10},
blocking=True)
assert on_off_cluster.request.call_count == 1
assert level_cluster.request.call_count == 1
assert on_off_cluster.request.call_args == call(
False, 1, (), expect_reply=True, manufacturer=None)
assert level_cluster.request.call_args == call(
False, 4, (types.uint8_t, types.uint16_t), 254, 100.0,
expect_reply=True, manufacturer=None)
on_off_cluster.request.reset_mock()
level_cluster.request.reset_mock()

await hass.services.async_call(DOMAIN, 'turn_on',
{'entity_id': entity_id, 'brightness': 10},
blocking=True)
assert on_off_cluster.request.call_count == 1
assert level_cluster.request.call_count == 1
assert on_off_cluster.request.call_args == call(
False, 1, (), expect_reply=True, manufacturer=None)
assert level_cluster.request.call_args == call(
False, 4, (types.uint8_t, types.uint16_t), 10, 5.0,
expect_reply=True, manufacturer=None)
on_off_cluster.request.reset_mock()
level_cluster.request.reset_mock()

await async_test_off_from_hass(hass, on_off_cluster, entity_id)


async def async_test_dimmer_from_light(hass, cluster, entity_id,
Expand Down

0 comments on commit 9066609

Please sign in to comment.