diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 81d44806c5e..c78b0c6f944 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -28,6 +28,8 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import RestoreEntity +from .utils import brightness_to_rflink + _LOGGER = logging.getLogger(__name__) ATTR_EVENT = "event" @@ -509,7 +511,7 @@ class RflinkCommand(RflinkDevice): elif command == "dim": # convert brightness to rflink dim level - cmd = str(int(args[0] / 17)) + cmd = str(brightness_to_rflink(args[0])) self._state = True elif command == "toggle": diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 2a97472f000..5a0d6766179 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -1,5 +1,6 @@ """Support for Rflink lights.""" import logging +import re import voluptuous as vol @@ -27,6 +28,7 @@ from . import ( EVENT_KEY_ID, SwitchableRflinkDevice, ) +from .utils import brightness_to_rflink, rflink_to_brightness _LOGGER = logging.getLogger(__name__) @@ -183,30 +185,39 @@ class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity): """Turn the device on.""" if ATTR_BRIGHTNESS in kwargs: # rflink only support 16 brightness levels - self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17 + self._brightness = rflink_to_brightness( + brightness_to_rflink(kwargs[ATTR_BRIGHTNESS]) + ) # Turn on light at the requested dim level await self._async_handle_command("dim", self._brightness) + def _handle_event(self, event): + """Adjust state if Rflink picks up a remote command for this device.""" + self.cancel_queued_send_commands() + + command = event["command"] + if command in ["on", "allon"]: + self._state = True + elif command in ["off", "alloff"]: + self._state = False + # dimmable device accept 'set_level=(0-15)' commands + elif re.search("^set_level=(0?[0-9]|1[0-5])$", command, re.IGNORECASE): + self._brightness = rflink_to_brightness(int(command.split("=")[1])) + self._state = True + @property def brightness(self): """Return the brightness of this light between 0..255.""" return self._brightness - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - if self._brightness is None: - return {} - return {ATTR_BRIGHTNESS: self._brightness} - @property def supported_features(self): """Flag supported features.""" return SUPPORT_BRIGHTNESS -class HybridRflinkLight(SwitchableRflinkDevice, LightEntity): +class HybridRflinkLight(DimmableRflinkLight, LightEntity): """Rflink light device that sends out both dim and on/off commands. Used for protocols which support lights that are not exclusively on/off @@ -221,52 +232,14 @@ class HybridRflinkLight(SwitchableRflinkDevice, LightEntity): Which results in a nice house disco :) """ - _brightness = 255 - - async def async_added_to_hass(self): - """Restore RFLink light brightness attribute.""" - await super().async_added_to_hass() - - old_state = await self.async_get_last_state() - if ( - old_state is not None - and old_state.attributes.get(ATTR_BRIGHTNESS) is not None - ): - # restore also brightness in dimmables devices - self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) - async def async_turn_on(self, **kwargs): """Turn the device on and set dim level.""" - if ATTR_BRIGHTNESS in kwargs: - # rflink only support 16 brightness levels - self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17 - - # if receiver supports dimming this will turn on the light - # at the requested dim level - await self._async_handle_command("dim", self._brightness) - + await super().async_turn_on(**kwargs) # if the receiving device does not support dimlevel this # will ensure it is turned on when full brightness is set - if self._brightness == 255: + if self.brightness == 255: await self._async_handle_command("turn_on") - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - if self._brightness is None: - return {} - return {ATTR_BRIGHTNESS: self._brightness} - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - class ToggleRflinkLight(SwitchableRflinkDevice, LightEntity): """Rflink light device which sends out only 'on' commands. diff --git a/homeassistant/components/rflink/utils.py b/homeassistant/components/rflink/utils.py new file mode 100644 index 00000000000..9738d9f74fa --- /dev/null +++ b/homeassistant/components/rflink/utils.py @@ -0,0 +1,11 @@ +"""RFLink integration utils.""" + + +def brightness_to_rflink(brightness: int) -> int: + """Convert 0-255 brightness to RFLink dim level (0-15).""" + return int(brightness / 17) + + +def rflink_to_brightness(dim_level: int) -> int: + """Convert RFLink dim level (0-15) to 0-255 brightness.""" + return int(dim_level * 17) diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index 79fa752e54c..5f9672ac9fc 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -340,6 +340,93 @@ async def test_type_toggle(hass, monkeypatch): assert hass.states.get(f"{DOMAIN}.toggle_test").state == "off" +async def test_set_level_command(hass, monkeypatch): + """Test 'set_level=XX' events.""" + config = { + "rflink": {"port": "/dev/ttyABC0"}, + DOMAIN: { + "platform": "rflink", + "devices": { + "newkaku_12345678_0": {"name": "l1"}, + "test_no_dimmable": {"name": "l2"}, + "test_dimmable": {"name": "l3", "type": "dimmable"}, + "test_hybrid": {"name": "l4", "type": "hybrid"}, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch) + + # test sending command to a newkaku device + event_callback({"id": "newkaku_12345678_0", "command": "set_level=10"}) + await hass.async_block_till_done() + # should affect state + state = hass.states.get(f"{DOMAIN}.l1") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 170 + # turn off + event_callback({"id": "newkaku_12345678_0", "command": "off"}) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.l1") + assert state + assert state.state == STATE_OFF + # off light shouldn't have brightness + assert not state.attributes.get(ATTR_BRIGHTNESS) + # turn on + event_callback({"id": "newkaku_12345678_0", "command": "on"}) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.l1") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 170 + + # test sending command to a no dimmable device + event_callback({"id": "test_no_dimmable", "command": "set_level=10"}) + await hass.async_block_till_done() + # should NOT affect state + state = hass.states.get(f"{DOMAIN}.l2") + assert state + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_BRIGHTNESS) + + # test sending command to a dimmable device + event_callback({"id": "test_dimmable", "command": "set_level=5"}) + await hass.async_block_till_done() + # should affect state + state = hass.states.get(f"{DOMAIN}.l3") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 85 + + # test sending command to a hybrid device + event_callback({"id": "test_hybrid", "command": "set_level=15"}) + await hass.async_block_till_done() + # should affect state + state = hass.states.get(f"{DOMAIN}.l4") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + + event_callback({"id": "test_hybrid", "command": "off"}) + await hass.async_block_till_done() + # should affect state + state = hass.states.get(f"{DOMAIN}.l4") + assert state + assert state.state == STATE_OFF + # off light shouldn't have brightness + assert not state.attributes.get(ATTR_BRIGHTNESS) + + event_callback({"id": "test_hybrid", "command": "set_level=0"}) + await hass.async_block_till_done() + # should affect state + state = hass.states.get(f"{DOMAIN}.l4") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 0 + + async def test_group_alias(hass, monkeypatch): """Group aliases should only respond to group commands (allon/alloff).""" config = { @@ -347,7 +434,12 @@ async def test_group_alias(hass, monkeypatch): DOMAIN: { "platform": "rflink", "devices": { - "protocol_0_0": {"name": "test", "group_aliases": ["test_group_0_0"]} + "protocol_0_0": {"name": "test", "group_aliases": ["test_group_0_0"]}, + "protocol_0_1": { + "name": "test2", + "type": "dimmable", + "group_aliases": ["test_group_0_0"], + }, }, }, } @@ -362,12 +454,14 @@ async def test_group_alias(hass, monkeypatch): await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == "on" + assert hass.states.get(f"{DOMAIN}.test2").state == "on" # test sending group command to group alias event_callback({"id": "test_group_0_0", "command": "off"}) await hass.async_block_till_done() assert hass.states.get(f"{DOMAIN}.test").state == "on" + assert hass.states.get(f"{DOMAIN}.test2").state == "on" async def test_nogroup_alias(hass, monkeypatch): @@ -396,7 +490,7 @@ async def test_nogroup_alias(hass, monkeypatch): # should not affect state assert hass.states.get(f"{DOMAIN}.test").state == "off" - # test sending group command to nogroup alias + # test sending group commands to nogroup alias event_callback({"id": "test_nogroup_0_0", "command": "on"}) await hass.async_block_till_done() # should affect state @@ -501,7 +595,8 @@ async def test_restore_state(hass, monkeypatch): state = hass.states.get(f"{DOMAIN}.l4") assert state assert state.state == STATE_OFF - assert state.attributes[ATTR_BRIGHTNESS] == 255 + # off light shouldn't have brightness + assert not state.attributes.get(ATTR_BRIGHTNESS) assert state.attributes["assumed_state"] # test coverage for dimmable light diff --git a/tests/components/rflink/test_utils.py b/tests/components/rflink/test_utils.py new file mode 100644 index 00000000000..50dd8100e8e --- /dev/null +++ b/tests/components/rflink/test_utils.py @@ -0,0 +1,33 @@ +"""Test for RFLink utils methods.""" +from homeassistant.components.rflink.utils import ( + brightness_to_rflink, + rflink_to_brightness, +) + + +async def test_utils(hass, monkeypatch): + """Test all utils methods.""" + # test brightness_to_rflink + assert brightness_to_rflink(0) == 0 + assert brightness_to_rflink(17) == 1 + assert brightness_to_rflink(34) == 2 + assert brightness_to_rflink(85) == 5 + assert brightness_to_rflink(170) == 10 + assert brightness_to_rflink(255) == 15 + + assert brightness_to_rflink(10) == 0 + assert brightness_to_rflink(20) == 1 + assert brightness_to_rflink(30) == 1 + assert brightness_to_rflink(40) == 2 + assert brightness_to_rflink(50) == 2 + assert brightness_to_rflink(60) == 3 + assert brightness_to_rflink(70) == 4 + assert brightness_to_rflink(80) == 4 + + # test rflink_to_brightness + assert rflink_to_brightness(0) == 0 + assert rflink_to_brightness(1) == 17 + assert rflink_to_brightness(5) == 85 + assert rflink_to_brightness(10) == 170 + assert rflink_to_brightness(12) == 204 + assert rflink_to_brightness(15) == 255