diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index 1682ef2ae02..4d9c72df2f1 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -34,10 +34,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info[ATTR_DISCOVER_AREAS] is None): return - devices = [SpcAlarm(hass=hass, - area_id=area['id'], - name=area['name'], - state=_get_alarm_state(area['mode'])) + api = hass.data[DATA_API] + devices = [SpcAlarm(api, area) for area in discovery_info[ATTR_DISCOVER_AREAS]] async_add_devices(devices) @@ -46,21 +44,29 @@ def async_setup_platform(hass, config, async_add_devices, class SpcAlarm(alarm.AlarmControlPanel): """Represents the SPC alarm panel.""" - def __init__(self, hass, area_id, name, state): + def __init__(self, api, area): """Initialize the SPC alarm panel.""" - self._hass = hass - self._area_id = area_id - self._name = name - self._state = state - self._api = hass.data[DATA_API] - - hass.data[DATA_REGISTRY].register_alarm_device(area_id, self) + self._area_id = area['id'] + self._name = area['name'] + self._state = _get_alarm_state(area['mode']) + if self._state == STATE_ALARM_DISARMED: + self._changed_by = area.get('last_unset_user_name', 'unknown') + else: + self._changed_by = area.get('last_set_user_name', 'unknown') + self._api = api @asyncio.coroutine - def async_update_from_spc(self, state): + def async_added_to_hass(self): + """Calbback for init handlers.""" + self.hass.data[DATA_REGISTRY].register_alarm_device( + self._area_id, self) + + @asyncio.coroutine + def async_update_from_spc(self, state, extra): """Update the alarm panel with a new state.""" self._state = state - yield from self.async_update_ha_state() + self._changed_by = extra.get('changed_by', 'unknown') + self.async_schedule_update_ha_state() @property def should_poll(self): @@ -72,6 +78,11 @@ class SpcAlarm(alarm.AlarmControlPanel): """Return the name of the device.""" return self._name + @property + def changed_by(self): + """Return the user the last change was triggered by.""" + return self._changed_by + @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py index af3669c2b15..a3a84580edd 100644 --- a/homeassistant/components/binary_sensor/spc.py +++ b/homeassistant/components/binary_sensor/spc.py @@ -67,7 +67,7 @@ class SpcBinarySensor(BinarySensorDevice): spc_registry.register_sensor_device(zone_id, self) @asyncio.coroutine - def async_update_from_spc(self, state): + def async_update_from_spc(self, state, extra): """Update the state of the device.""" self._state = state yield from self.async_update_ha_state() diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index a271297d0fd..c186559c91a 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -87,9 +87,14 @@ def _async_process_message(sia_message, spc_registry): # ZX - Zone Short # ZD - Zone Disconnected - if sia_code in ('BA', 'CG', 'NL', 'OG', 'OQ'): + extra = {} + + if sia_code in ('BA', 'CG', 'NL', 'OG'): # change in area status, notify alarm panel device device = spc_registry.get_alarm_device(spc_id) + data = sia_message['description'].split('¦') + if len(data) == 3: + extra['changed_by'] = data[1] else: # change in zone status, notify sensor device device = spc_registry.get_sensor_device(spc_id) @@ -98,7 +103,6 @@ def _async_process_message(sia_message, spc_registry): 'CG': STATE_ALARM_ARMED_AWAY, 'NL': STATE_ALARM_ARMED_HOME, 'OG': STATE_ALARM_DISARMED, - 'OQ': STATE_ALARM_DISARMED, 'ZO': STATE_ON, 'ZC': STATE_OFF, 'ZX': STATE_UNKNOWN, @@ -110,7 +114,7 @@ def _async_process_message(sia_message, spc_registry): _LOGGER.warning("No device mapping found for SPC area/zone id %s.", spc_id) elif new_state: - yield from device.async_update_from_spc(new_state) + yield from device.async_update_from_spc(new_state, extra) class SpcRegistry: diff --git a/tests/components/alarm_control_panel/test_spc.py b/tests/components/alarm_control_panel/test_spc.py index 504b4e9237c..63b79781404 100644 --- a/tests/components/alarm_control_panel/test_spc.py +++ b/tests/components/alarm_control_panel/test_spc.py @@ -7,7 +7,7 @@ from homeassistant.components.spc import SpcRegistry from homeassistant.components.alarm_control_panel import spc from tests.common import async_test_home_assistant from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED) @pytest.fixture @@ -38,19 +38,19 @@ def test_setup_platform(hass): 'last_set_user_name': 'Pelle', 'last_unset_time': '1485800564', 'last_unset_user_id': '1', - 'last_unset_user_name': 'Pelle', + 'last_unset_user_name': 'Lisa', 'last_alarm': '1478174896' - }, { + }, { 'id': '3', 'name': 'Garage', 'mode': '0', 'last_set_time': '1483705803', 'last_set_user_id': '9998', - 'last_set_user_name': 'Lisa', + 'last_set_user_name': 'Pelle', 'last_unset_time': '1483705808', 'last_unset_user_id': '9998', 'last_unset_user_name': 'Lisa' - }]} + }]} yield from spc.async_setup_platform(hass=hass, config={}, @@ -58,7 +58,11 @@ def test_setup_platform(hass): discovery_info=areas) assert len(added_entities) == 2 + assert added_entities[0].name == 'House' assert added_entities[0].state == STATE_ALARM_ARMED_AWAY + assert added_entities[0].changed_by == 'Pelle' + assert added_entities[1].name == 'Garage' assert added_entities[1].state == STATE_ALARM_DISARMED + assert added_entities[1].changed_by == 'Lisa' diff --git a/tests/components/binary_sensor/test_spc.py b/tests/components/binary_sensor/test_spc.py index 5004ccd3210..d2299874527 100644 --- a/tests/components/binary_sensor/test_spc.py +++ b/tests/components/binary_sensor/test_spc.py @@ -30,7 +30,7 @@ def test_setup_platform(hass): 'area_name': 'House', 'input': '0', 'status': '0', - }, { + }, { 'id': '3', 'type': '0', 'zone_name': 'Hallway PIR', @@ -38,7 +38,7 @@ def test_setup_platform(hass): 'area_name': 'House', 'input': '0', 'status': '0', - }, { + }, { 'id': '5', 'type': '1', 'zone_name': 'Front door', @@ -46,7 +46,7 @@ def test_setup_platform(hass): 'area_name': 'House', 'input': '1', 'status': '0', - }]} + }]} def add_entities(entities): nonlocal added_entities diff --git a/tests/components/test_spc.py b/tests/components/test_spc.py index 6fae8d821c2..7837abd8007 100644 --- a/tests/components/test_spc.py +++ b/tests/components/test_spc.py @@ -7,7 +7,9 @@ from homeassistant.components import spc from homeassistant.bootstrap import async_setup_component from tests.common import async_test_home_assistant from tests.test_util.aiohttp import mock_aiohttp_client -from homeassistant.const import (STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) @pytest.fixture @@ -57,7 +59,13 @@ def aioclient_mock(): @asyncio.coroutine -def test_update_alarm_device(hass, aioclient_mock, monkeypatch): +@pytest.mark.parametrize("sia_code,state", [ + ('NL', STATE_ALARM_ARMED_HOME), + ('CG', STATE_ALARM_ARMED_AWAY), + ('OG', STATE_ALARM_DISARMED) +]) +def test_update_alarm_device(hass, aioclient_mock, monkeypatch, + sia_code, state): """Test that alarm panel state changes on incoming websocket data.""" monkeypatch.setattr("homeassistant.components.spc.SpcWebGateway." "start_listener", lambda x, *args: None) @@ -65,8 +73,8 @@ def test_update_alarm_device(hass, aioclient_mock, monkeypatch): 'spc': { 'api_url': 'http://localhost/', 'ws_url': 'ws://localhost/' - } } + } yield from async_setup_component(hass, 'spc', config) yield from hass.async_block_till_done() @@ -74,38 +82,48 @@ def test_update_alarm_device(hass, aioclient_mock, monkeypatch): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED - msg = {"sia_code": "NL", "sia_address": "1", "description": "House|Sam|1"} + msg = {"sia_code": sia_code, "sia_address": "1", + "description": "House¦Sam¦1"} yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY]) - assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME + yield from hass.async_block_till_done() - msg = {"sia_code": "OQ", "sia_address": "1", "description": "Sam"} - yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY]) - assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED + state_obj = hass.states.get(entity_id) + assert state_obj.state == state + assert state_obj.attributes['changed_by'] == 'Sam' @asyncio.coroutine -def test_update_sensor_device(hass, aioclient_mock, monkeypatch): - """Test that sensors change state on incoming websocket data.""" +@pytest.mark.parametrize("sia_code,state", [ + ('ZO', STATE_ON), + ('ZC', STATE_OFF) +]) +def test_update_sensor_device(hass, aioclient_mock, monkeypatch, + sia_code, state): + """ + Test that sensors change state on incoming websocket data. + + Note that we don't test for the ZD (disconnected) and ZX (problem/short) + codes since the binary sensor component is hardcoded to only + let on/off states through. + """ monkeypatch.setattr("homeassistant.components.spc.SpcWebGateway." "start_listener", lambda x, *args: None) config = { 'spc': { 'api_url': 'http://localhost/', 'ws_url': 'ws://localhost/' - } } + } yield from async_setup_component(hass, 'spc', config) yield from hass.async_block_till_done() - assert hass.states.get('binary_sensor.hallway_pir').state == 'off' + assert hass.states.get('binary_sensor.hallway_pir').state == STATE_OFF - msg = {"sia_code": "ZO", "sia_address": "3", "description": "Hallway PIR"} + msg = {"sia_code": sia_code, "sia_address": "3", + "description": "Hallway PIR"} yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY]) - assert hass.states.get('binary_sensor.hallway_pir').state == 'on' - - msg = {"sia_code": "ZC", "sia_address": "3", "description": "Hallway PIR"} - yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY]) - assert hass.states.get('binary_sensor.hallway_pir').state == 'off' + yield from hass.async_block_till_done() + assert hass.states.get('binary_sensor.hallway_pir').state == state class TestSpcRegistry: @@ -139,7 +157,7 @@ class TestSpcWebGateway: ('set', spc.SpcWebGateway.AREA_COMMAND_SET), ('unset', spc.SpcWebGateway.AREA_COMMAND_UNSET), ('set_a', spc.SpcWebGateway.AREA_COMMAND_PART_SET) - ]) + ]) def test_area_commands(self, spcwebgw, url_command, command): """Test alarm arming/disarming.""" with mock_aiohttp_client() as aioclient_mock: