From e1ed076015b0c8904caf208dbe556485131f963b Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Wed, 29 Mar 2017 07:04:25 +0200 Subject: [PATCH] Rflink group commands (#5969) * Add support for group commands (allon/alloff). Add 'group_aliasses' config attribute that only respond to group commands. Add nogroup_aliases that only respond to 'on' 'off' commands. Allow settings device id group behaviour. * Fix linting. * Fix lint. --- homeassistant/components/light/rflink.py | 32 ++++- homeassistant/components/rflink.py | 35 ++++-- requirements_all.txt | 2 +- tests/components/light/test_rflink.py | 148 +++++++++++++++++++++++ 4 files changed, 200 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index 0630792602d..ad25ae6a38d 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -11,11 +11,13 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.rflink import ( CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, - CONF_IGNORE_DEVICES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, - DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN, - EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol) + CONF_GROUP, CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES, + CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, + DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, + DOMAIN, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol) from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN) + DEPENDENCIES = ['rflink'] _LOGGER = logging.getLogger(__name__) @@ -38,8 +40,13 @@ PLATFORM_SCHEMA = vol.Schema({ TYPE_HYBRID, TYPE_TOGGLE), vol.Optional(CONF_ALIASSES, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_GROUP_ALIASSES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_NOGROUP_ALIASSES, default=[]): + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), + vol.Optional(CONF_GROUP, default=True): cv.boolean, }, }), }) @@ -110,7 +117,24 @@ def devices_from_config(domain_config, hass=None): devices.append(device) # Register entity (and aliasses) to listen to incoming rflink events - for _id in [device_id] + config[CONF_ALIASSES]: + + # device id and normal aliasses respond to normal and group command + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][device_id].append(device) + if config[CONF_GROUP]: + hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][device_id].append(device) + for _id in config[CONF_ALIASSES]: + hass.data[DATA_ENTITY_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(device) + hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(device) + # group_aliasses only respond to group commands + for _id in config[CONF_GROUP_ALIASSES]: + hass.data[DATA_ENTITY_GROUP_LOOKUP][ + EVENT_KEY_COMMAND][_id].append(device) + # nogroup_aliasses only respond to normal commands + for _id in config[CONF_NOGROUP_ALIASSES]: hass.data[DATA_ENTITY_LOOKUP][ EVENT_KEY_COMMAND][_id].append(device) diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index df1ca955a4d..2986b898ed0 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -20,7 +20,7 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['rflink==0.0.28'] +REQUIREMENTS = ['rflink==0.0.31'] _LOGGER = logging.getLogger(__name__) @@ -28,6 +28,9 @@ ATTR_EVENT = 'event' ATTR_STATE = 'state' CONF_ALIASSES = 'aliasses' +CONF_GROUP_ALIASSES = 'group_aliasses' +CONF_GROUP = 'group' +CONF_NOGROUP_ALIASSES = 'nogroup_aliasses' CONF_DEVICE_DEFAULTS = 'device_defaults' CONF_DEVICES = 'devices' CONF_FIRE_EVENT = 'fire_event' @@ -38,6 +41,7 @@ CONF_WAIT_FOR_ACK = 'wait_for_ack' DATA_DEVICE_REGISTER = 'rflink_device_register' DATA_ENTITY_LOOKUP = 'rflink_entity_lookup' +DATA_ENTITY_GROUP_LOOKUP = 'rflink_entity_group_only_lookup' DEFAULT_RECONNECT_INTERVAL = 10 DEFAULT_SIGNAL_REPETITIONS = 1 CONNECTION_TIMEOUT = 10 @@ -48,6 +52,8 @@ EVENT_KEY_ID = 'id' EVENT_KEY_SENSOR = 'sensor' EVENT_KEY_UNIT = 'unit' +RFLINK_GROUP_COMMANDS = ['allon', 'alloff'] + DOMAIN = 'rflink' DEVICE_DEFAULTS_SCHEMA = vol.Schema({ @@ -94,6 +100,10 @@ def async_setup(hass, config): EVENT_KEY_COMMAND: defaultdict(list), EVENT_KEY_SENSOR: defaultdict(list), } + hass.data[DATA_ENTITY_GROUP_LOOKUP] = { + EVENT_KEY_COMMAND: defaultdict(list), + EVENT_KEY_SENSOR: defaultdict(list), + } # Allow platform to specify function to register new unknown devices hass.data[DATA_DEVICE_REGISTER] = {} @@ -116,7 +126,14 @@ def async_setup(hass, config): # Lookup entities who registered this device id as device id or alias event_id = event.get('id', None) - entities = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id] + + is_group_event = (event_type == EVENT_KEY_COMMAND and + event[EVENT_KEY_COMMAND] in RFLINK_GROUP_COMMANDS) + if is_group_event: + entities = hass.data[DATA_ENTITY_GROUP_LOOKUP][event_type].get( + event_id, []) + else: + entities = hass.data[DATA_ENTITY_LOOKUP][event_type][event_id] if entities: # Propagate event to every entity matching the device id @@ -202,8 +219,8 @@ class RflinkDevice(Entity): platform = None _state = STATE_UNKNOWN - def __init__(self, device_id, hass, name=None, - aliasses=None, fire_event=False, + def __init__(self, device_id, hass, name=None, aliasses=None, group=True, + group_aliasses=None, nogroup_aliasses=None, fire_event=False, signal_repetitions=DEFAULT_SIGNAL_REPETITIONS): """Initialize the device.""" self.hass = hass @@ -215,12 +232,6 @@ class RflinkDevice(Entity): else: self._name = device_id - # Generate list of device_ids to match against - if aliasses: - self._aliasses = aliasses - else: - self._aliasses = [] - self._should_fire_event = fire_event self._signal_repetitions = signal_repetitions @@ -375,9 +386,9 @@ class SwitchableRflinkDevice(RflinkCommand): self.cancel_queued_send_commands() command = event['command'] - if command == 'on': + if command in ['on', 'allon']: self._state = True - elif command == 'off': + elif command in ['off', 'alloff']: self._state = False def async_turn_on(self, **kwargs): diff --git a/requirements_all.txt b/requirements_all.txt index ea986259ea8..dac5add5c94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ qnapstats==0.2.3 radiotherm==1.2 # homeassistant.components.rflink -rflink==0.0.28 +rflink==0.0.31 # homeassistant.components.sensor.ring ring_doorbell==0.1.0 diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py index 2cd6d6fd092..9f1d98b0ffd 100644 --- a/tests/components/light/test_rflink.py +++ b/tests/components/light/test_rflink.py @@ -81,6 +81,25 @@ def test_default_setup(hass, monkeypatch): assert hass.states.get('light.test').state == 'off' + # should repond to group command + event_callback({ + 'id': 'protocol_0_0', + 'command': 'allon', + }) + yield from hass.async_block_till_done() + + light_after_first_command = hass.states.get('light.test') + assert light_after_first_command.state == 'on' + + # should repond to group command + event_callback({ + 'id': 'protocol_0_0', + 'command': 'alloff', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('light.test').state == 'off' + # test following aliasses # mock incoming command event for this device alias event_callback({ @@ -385,3 +404,132 @@ def test_type_toggle(hass, monkeypatch): yield from hass.async_block_till_done() assert hass.states.get('light.toggle_test').state == 'off' + + +@asyncio.coroutine +def test_group_alias(hass, monkeypatch): + """Group aliases should only respond to group commands (allon/alloff).""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'group_aliasses': ['test_group_0_0'], + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + assert hass.states.get('light.test').state == 'off' + + # test sending group command to group alias + event_callback({ + 'id': 'test_group_0_0', + 'command': 'allon', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('light.test').state == 'on' + + # test sending group command to group alias + event_callback({ + 'id': 'test_group_0_0', + 'command': 'off', + }) + yield from hass.async_block_till_done() + + assert hass.states.get('light.test').state == 'on' + + +@asyncio.coroutine +def test_nogroup_alias(hass, monkeypatch): + """Non group aliases should not respond to group commands.""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'protocol_0_0': { + 'name': 'test', + 'nogroup_aliasses': ['test_nogroup_0_0'], + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + assert hass.states.get('light.test').state == 'off' + + # test sending group command to nogroup alias + event_callback({ + 'id': 'test_nogroup_0_0', + 'command': 'allon', + }) + yield from hass.async_block_till_done() + # should not affect state + assert hass.states.get('light.test').state == 'off' + + # test sending group command to nogroup alias + event_callback({ + 'id': 'test_nogroup_0_0', + 'command': 'on', + }) + yield from hass.async_block_till_done() + # should affect state + assert hass.states.get('light.test').state == 'on' + + +@asyncio.coroutine +def test_nogroup_device_id(hass, monkeypatch): + """Device id that do not respond to group commands (allon/alloff).""" + config = { + 'rflink': { + 'port': '/dev/ttyABC0', + }, + DOMAIN: { + 'platform': 'rflink', + 'devices': { + 'test_nogroup_0_0': { + 'name': 'test', + 'group': False, + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, _, _ = yield from mock_rflink( + hass, config, DOMAIN, monkeypatch) + + assert hass.states.get('light.test').state == 'off' + + # test sending group command to nogroup + event_callback({ + 'id': 'test_nogroup_0_0', + 'command': 'allon', + }) + yield from hass.async_block_till_done() + # should not affect state + assert hass.states.get('light.test').state == 'off' + + # test sending group command to nogroup + event_callback({ + 'id': 'test_nogroup_0_0', + 'command': 'on', + }) + yield from hass.async_block_till_done() + # should affect state + assert hass.states.get('light.test').state == 'on'