From 5b77a357e6698fe15534772f420389d77c4c5dca Mon Sep 17 00:00:00 2001 From: fmartens <17504441+fmartens@users.noreply.github.com> Date: Sun, 1 Sep 2019 17:52:43 +0200 Subject: [PATCH] Inverted rflink cover (#26038) * Added InvertedRflinkCover class to support COCO/KAKU ASUN-650 devices * Rename TYPE_NORMAL to TYPE_STANDARD * Cleaning up code and removed unused imports * Added unit tests for InvertedRflinkCover * less if/else statements * Autoresolve type for newkaku * Updated tests for InvertedRflinkCover * Added unit test for standard cover without specifying type * Updated comments in unit tests * Updated unit test configuration and comments to be more explanatory * Restore variable names in first part of the unit test that have been changed during a search and replace * Reformated the code according to 4de97ab * remove blank lines at end of rflink test_cover.py * Replace single with double quote in test_cover.py * Replaced single quotes with double qoutes and fixed formatting * Black improvements * Reformated the code of the unit test. * entity_type_for_device_id should return 'TYPE_STANDARD' instead of 'None' --- homeassistant/components/rflink/cover.py | 56 ++- tests/components/rflink/test_cover.py | 412 +++++++++++++++++++++++ 2 files changed, 466 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 7e6de0ec03b..f41c4cde2f7 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice -from homeassistant.const import CONF_NAME, STATE_OPEN +from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_OPEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -23,6 +23,8 @@ from . import ( _LOGGER = logging.getLogger(__name__) +TYPE_STANDARD = "standard" +TYPE_INVERTED = "inverted" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -33,6 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { cv.string: { vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TYPE): vol.Any(TYPE_STANDARD, TYPE_INVERTED), vol.Optional(CONF_ALIASES, default=[]): vol.All( cv.ensure_list, [cv.string] ), @@ -52,12 +55,51 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +def entity_type_for_device_id(device_id): + """Return entity class for protocol of a given device_id. + + Async friendly. + """ + entity_type_mapping = { + # KlikAanKlikUit cover have the controls inverted + "newkaku": TYPE_INVERTED + } + protocol = device_id.split("_")[0] + return entity_type_mapping.get(protocol, TYPE_STANDARD) + + +def entity_class_for_type(entity_type): + """Translate entity type to entity class. + + Async friendly. + """ + entity_device_mapping = { + # default cover implementation + TYPE_STANDARD: RflinkCover, + # cover with open/close commands inverted + # like KAKU/COCO ASUN-650 + TYPE_INVERTED: InvertedRflinkCover, + } + + return entity_device_mapping.get(entity_type, RflinkCover) + + def devices_from_config(domain_config): """Parse configuration and add Rflink cover devices.""" devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): + # Determine what kind of entity to create, RflinkCover + # or InvertedRflinkCover + if CONF_TYPE in config: + # Remove type from config to not pass it as and argument + # to entity instantiation + entity_type = config.pop(CONF_TYPE) + else: + entity_type = entity_type_for_device_id(device_id) + + entity_class = entity_class_for_type(entity_type) device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - device = RflinkCover(device_id, **device_config) + device = entity_class(device_id, **device_config) devices.append(device) return devices @@ -115,3 +157,13 @@ class RflinkCover(RflinkCommand, CoverDevice, RestoreEntity): def async_stop_cover(self, **kwargs): """Turn the device stop.""" return self._async_handle_command("stop_cover") + + +class InvertedRflinkCover(RflinkCover): + """Rflink cover that has inverted open/close commands.""" + + async def _async_send_command(self, cmd, repetitions): + """Will invert only the UP/DOWN commands.""" + _LOGGER.debug("Getting command: %s for Rflink device: %s", cmd, self._device_id) + cmd_inv = {"UP": "DOWN", "DOWN": "UP"} + await super()._async_send_command(cmd_inv.get(cmd, cmd), repetitions) diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index e4b3154a4c4..858258e7efd 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -390,3 +390,415 @@ async def test_restore_state(hass, monkeypatch): assert state assert state.state == STATE_CLOSED assert state.attributes["assumed_state"] + + +# The code checks the ID, it will use the +# 'inverted' class when the name starts with +# 'newkaku' +async def test_inverted_cover(hass, monkeypatch): + """Ensure states are restored on startup.""" + config = { + "rflink": {"port": "/dev/ttyABC0"}, + DOMAIN: { + "platform": "rflink", + "devices": { + "nonkaku_device_1": { + "name": "nonkaku_type_standard", + "type": "standard", + }, + "nonkaku_device_2": {"name": "nonkaku_type_none"}, + "nonkaku_device_3": { + "name": "nonkaku_type_inverted", + "type": "inverted", + }, + "newkaku_device_4": { + "name": "newkaku_type_standard", + "type": "standard", + }, + "newkaku_device_5": {"name": "newkaku_type_none"}, + "newkaku_device_6": { + "name": "newkaku_type_inverted", + "type": "inverted", + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, protocol, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch + ) + + # test default state of cover loaded from config + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_standard") + assert standard_cover.state == STATE_CLOSED + assert standard_cover.attributes["assumed_state"] + + # mock incoming up command event for nonkaku_device_1 + event_callback({"id": "nonkaku_device_1", "command": "up"}) + await hass.async_block_till_done() + + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_standard") + assert standard_cover.state == STATE_OPEN + assert standard_cover.attributes.get("assumed_state") + + # mock incoming up command event for nonkaku_device_2 + event_callback({"id": "nonkaku_device_2", "command": "up"}) + await hass.async_block_till_done() + + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_none") + assert standard_cover.state == STATE_OPEN + assert standard_cover.attributes.get("assumed_state") + + # mock incoming up command event for nonkaku_device_3 + event_callback({"id": "nonkaku_device_3", "command": "up"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".nonkaku_type_inverted") + assert inverted_cover.state == STATE_OPEN + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming up command event for newkaku_device_4 + event_callback({"id": "newkaku_device_4", "command": "up"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_standard") + assert inverted_cover.state == STATE_OPEN + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming up command event for newkaku_device_5 + event_callback({"id": "newkaku_device_5", "command": "up"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_none") + assert inverted_cover.state == STATE_OPEN + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming up command event for newkaku_device_6 + event_callback({"id": "newkaku_device_6", "command": "up"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_inverted") + assert inverted_cover.state == STATE_OPEN + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming down command event for nonkaku_device_1 + event_callback({"id": "nonkaku_device_1", "command": "down"}) + + await hass.async_block_till_done() + + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_standard") + assert standard_cover.state == STATE_CLOSED + assert standard_cover.attributes.get("assumed_state") + + # mock incoming down command event for nonkaku_device_2 + event_callback({"id": "nonkaku_device_2", "command": "down"}) + + await hass.async_block_till_done() + + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_none") + assert standard_cover.state == STATE_CLOSED + assert standard_cover.attributes.get("assumed_state") + + # mock incoming down command event for nonkaku_device_3 + event_callback({"id": "nonkaku_device_3", "command": "down"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".nonkaku_type_inverted") + assert inverted_cover.state == STATE_CLOSED + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming down command event for newkaku_device_4 + event_callback({"id": "newkaku_device_4", "command": "down"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_standard") + assert inverted_cover.state == STATE_CLOSED + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming down command event for newkaku_device_5 + event_callback({"id": "newkaku_device_5", "command": "down"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_none") + assert inverted_cover.state == STATE_CLOSED + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming down command event for newkaku_device_6 + event_callback({"id": "newkaku_device_6", "command": "down"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_inverted") + assert inverted_cover.state == STATE_CLOSED + assert inverted_cover.attributes.get("assumed_state") + + # We are only testing the 'inverted' devices, the 'standard' devices + # are already covered by other test cases. + + # should respond to group command + event_callback({"id": "nonkaku_device_3", "command": "alloff"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".nonkaku_type_inverted") + assert inverted_cover.state == STATE_CLOSED + + # should respond to group command + event_callback({"id": "nonkaku_device_3", "command": "allon"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".nonkaku_type_inverted") + assert inverted_cover.state == STATE_OPEN + + # should respond to group command + event_callback({"id": "newkaku_device_4", "command": "alloff"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_standard") + assert inverted_cover.state == STATE_CLOSED + + # should respond to group command + event_callback({"id": "newkaku_device_4", "command": "allon"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_standard") + assert inverted_cover.state == STATE_OPEN + + # should respond to group command + event_callback({"id": "newkaku_device_5", "command": "alloff"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_none") + assert inverted_cover.state == STATE_CLOSED + + # should respond to group command + event_callback({"id": "newkaku_device_5", "command": "allon"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_none") + assert inverted_cover.state == STATE_OPEN + + # should respond to group command + event_callback({"id": "newkaku_device_6", "command": "alloff"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_inverted") + assert inverted_cover.state == STATE_CLOSED + + # should respond to group command + event_callback({"id": "newkaku_device_6", "command": "allon"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_inverted") + assert inverted_cover.state == STATE_OPEN + + # Sending the close command from HA should result + # in an 'DOWN' command sent to a non-newkaku device + # that has its type set to 'standard'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_standard"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_standard").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[0][0][0] == "nonkaku_device_1" + assert protocol.send_command_ack.call_args_list[0][0][1] == "DOWN" + + # Sending the open command from HA should result + # in an 'UP' command sent to a non-newkaku device + # that has its type set to 'standard'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_standard"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_standard").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[1][0][0] == "nonkaku_device_1" + assert protocol.send_command_ack.call_args_list[1][0][1] == "UP" + + # Sending the close command from HA should result + # in an 'DOWN' command sent to a non-newkaku device + # that has its type not specified. + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_none"} + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_none").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[2][0][0] == "nonkaku_device_2" + assert protocol.send_command_ack.call_args_list[2][0][1] == "DOWN" + + # Sending the open command from HA should result + # in an 'UP' command sent to a non-newkaku device + # that has its type not specified. + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_none"} + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_none").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[3][0][0] == "nonkaku_device_2" + assert protocol.send_command_ack.call_args_list[3][0][1] == "UP" + + # Sending the close command from HA should result + # in an 'UP' command sent to a non-newkaku device + # that has its type set to 'inverted'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_inverted"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_inverted").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[4][0][0] == "nonkaku_device_3" + assert protocol.send_command_ack.call_args_list[4][0][1] == "UP" + + # Sending the open command from HA should result + # in an 'DOWN' command sent to a non-newkaku device + # that has its type set to 'inverted'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_inverted"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_inverted").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[5][0][0] == "nonkaku_device_3" + assert protocol.send_command_ack.call_args_list[5][0][1] == "DOWN" + + # Sending the close command from HA should result + # in an 'DOWN' command sent to a newkaku device + # that has its type set to 'standard'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_standard"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_standard").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[6][0][0] == "newkaku_device_4" + assert protocol.send_command_ack.call_args_list[6][0][1] == "DOWN" + + # Sending the open command from HA should result + # in an 'UP' command sent to a newkaku device + # that has its type set to 'standard'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_standard"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_standard").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[7][0][0] == "newkaku_device_4" + assert protocol.send_command_ack.call_args_list[7][0][1] == "UP" + + # Sending the close command from HA should result + # in an 'UP' command sent to a newkaku device + # that has its type not specified. + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_none"} + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_none").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[8][0][0] == "newkaku_device_5" + assert protocol.send_command_ack.call_args_list[8][0][1] == "UP" + + # Sending the open command from HA should result + # in an 'DOWN' command sent to a newkaku device + # that has its type not specified. + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_none"} + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_none").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[9][0][0] == "newkaku_device_5" + assert protocol.send_command_ack.call_args_list[9][0][1] == "DOWN" + + # Sending the close command from HA should result + # in an 'UP' command sent to a newkaku device + # that has its type set to 'inverted'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_inverted"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_inverted").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[10][0][0] == "newkaku_device_6" + assert protocol.send_command_ack.call_args_list[10][0][1] == "UP" + + # Sending the open command from HA should result + # in an 'DOWN' command sent to a newkaku device + # that has its type set to 'inverted'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_inverted"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_inverted").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[11][0][0] == "newkaku_device_6" + assert protocol.send_command_ack.call_args_list[11][0][1] == "DOWN"