diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 632673233ec..d7c17a1ccff 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMING, STATE_ALARM_TRIGGERED, ) +from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -114,16 +115,20 @@ class TotalConnectAlarm(alarm.AlarmControlPanelEntity): def alarm_disarm(self, code=None): """Send disarm command.""" - self._client.disarm(self._location_id) + if self._client.disarm(self._location_id) is not True: + raise HomeAssistantError(f"TotalConnect failed to disarm {self._name}.") def alarm_arm_home(self, code=None): """Send arm home command.""" - self._client.arm_stay(self._location_id) + if self._client.arm_stay(self._location_id) is not True: + raise HomeAssistantError(f"TotalConnect failed to arm home {self._name}.") def alarm_arm_away(self, code=None): """Send arm away command.""" - self._client.arm_away(self._location_id) + if self._client.arm_away(self._location_id) is not True: + raise HomeAssistantError(f"TotalConnect failed to arm away {self._name}.") def alarm_arm_night(self, code=None): """Send arm night command.""" - self._client.arm_stay_night(self._location_id) + if self._client.arm_stay_night(self._location_id) is not True: + raise HomeAssistantError(f"TotalConnect failed to arm night {self._name}.") diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index fc19c889d8b..665a42aba1a 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Honeywell Total Connect Alarm", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==0.54.1"], + "requirements": ["total_connect_client==0.55"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 00b309569b1..dcbdbf8deb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2102,7 +2102,7 @@ todoist-python==8.0.0 toonapilib==3.2.4 # homeassistant.components.totalconnect -total_connect_client==0.54.1 +total_connect_client==0.55 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7348e7f256..04ed4863511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -853,7 +853,7 @@ teslajsonpy==0.8.1 toonapilib==3.2.4 # homeassistant.components.totalconnect -total_connect_client==0.54.1 +total_connect_client==0.55 # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py new file mode 100644 index 00000000000..c2d6f92015c --- /dev/null +++ b/tests/components/totalconnect/common.py @@ -0,0 +1,129 @@ +"""Common methods used across tests for TotalConnect.""" +from total_connect_client import TotalConnectClient + +from homeassistant.components.totalconnect import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +LOCATION_INFO_BASIC_NORMAL = { + "LocationID": "123456", + "LocationName": "test", + "SecurityDeviceID": "987654", + "PhotoURL": "http://www.example.com/some/path/to/file.jpg", + "LocationModuleFlags": "Security=1,Video=0,Automation=0,GPS=0,VideoPIR=0", + "DeviceList": None, +} + +LOCATIONS = {"LocationInfoBasic": [LOCATION_INFO_BASIC_NORMAL]} + +MODULE_FLAGS = "Some=0,Fake=1,Flags=2" + +USER = { + "UserID": "1234567", + "Username": "username", + "UserFeatureList": "Master=0,User Administration=0,Configuration Administration=0", +} + +RESPONSE_AUTHENTICATE = { + "ResultCode": 0, + "SessionID": 1, + "Locations": LOCATIONS, + "ModuleFlags": MODULE_FLAGS, + "UserInfo": USER, +} + +PARTITION_DISARMED = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.DISARMED, +} + +PARTITION_ARMED_STAY = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_STAY, +} + +PARTITION_ARMED_AWAY = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_AWAY, +} + +PARTITION_INFO_DISARMED = {0: PARTITION_DISARMED} +PARTITION_INFO_ARMED_STAY = {0: PARTITION_ARMED_STAY} +PARTITION_INFO_ARMED_AWAY = {0: PARTITION_ARMED_AWAY} + +PARTITIONS_DISARMED = {"PartitionInfo": PARTITION_INFO_DISARMED} +PARTITIONS_ARMED_STAY = {"PartitionInfo": PARTITION_INFO_ARMED_STAY} +PARTITIONS_ARMED_AWAY = {"PartitionInfo": PARTITION_INFO_ARMED_AWAY} + +ZONE_NORMAL = { + "ZoneID": "1", + "ZoneDescription": "Normal", + "ZoneStatus": TotalConnectClient.ZONE_STATUS_NORMAL, + "PartitionId": "1", +} + +ZONE_INFO = [ZONE_NORMAL] +ZONES = {"ZoneInfo": ZONE_INFO} + +METADATA_DISARMED = { + "Partitions": PARTITIONS_DISARMED, + "Zones": ZONES, + "PromptForImportSecuritySettings": False, + "IsInACLoss": False, + "IsCoverTampered": False, + "Bell1SupervisionFailure": False, + "Bell2SupervisionFailure": False, + "IsInLowBattery": False, +} + +METADATA_ARMED_STAY = METADATA_DISARMED.copy() +METADATA_ARMED_STAY["Partitions"] = PARTITIONS_ARMED_STAY + +METADATA_ARMED_AWAY = METADATA_DISARMED.copy() +METADATA_ARMED_AWAY["Partitions"] = PARTITIONS_ARMED_AWAY + +RESPONSE_DISARMED = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_DISARMED} +RESPONSE_ARMED_STAY = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_STAY} +RESPONSE_ARMED_AWAY = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_AWAY} + +RESPONSE_ARM_SUCCESS = {"ResultCode": TotalConnectClient.TotalConnectClient.ARM_SUCCESS} +RESPONSE_ARM_FAILURE = { + "ResultCode": TotalConnectClient.TotalConnectClient.COMMAND_FAILED +} +RESPONSE_DISARM_SUCCESS = { + "ResultCode": TotalConnectClient.TotalConnectClient.DISARM_SUCCESS +} +RESPONSE_DISARM_FAILURE = { + "ResultCode": TotalConnectClient.TotalConnectClient.COMMAND_FAILED, + "ResultData": "Command Failed", +} + + +async def setup_platform(hass, platform): + """Set up the TotalConnect platform.""" + # first set up a config entry and add it to hass + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ) + mock_entry.add_to_hass(hass) + + responses = [RESPONSE_AUTHENTICATE, RESPONSE_DISARMED] + + with patch("homeassistant.components.totalconnect.PLATFORMS", [platform]), patch( + "zeep.Client", autospec=True + ), patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ) as mock_request, patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.get_zone_details", + return_value=True, + ): + assert await async_setup_component(hass, DOMAIN, {}) + assert mock_request.call_count == 2 + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py new file mode 100644 index 00000000000..75e07f09bf7 --- /dev/null +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -0,0 +1,153 @@ +"""Tests for the TotalConnect alarm control panel device.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_DISARM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) + +from .common import ( + RESPONSE_ARM_FAILURE, + RESPONSE_ARM_SUCCESS, + RESPONSE_ARMED_AWAY, + RESPONSE_ARMED_STAY, + RESPONSE_DISARM_FAILURE, + RESPONSE_DISARM_SUCCESS, + RESPONSE_DISARMED, + setup_platform, +) + +from tests.async_mock import patch + +ENTITY_ID = "alarm_control_panel.test" +CODE = "-1" +DATA = {ATTR_ENTITY_ID: ENTITY_ID} + + +async def test_attributes(hass): + """Test the alarm control panel attributes are correct.""" + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + return_value=RESPONSE_DISARMED, + ) as mock_request: + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_DISARMED + mock_request.assert_called_once() + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + + +async def test_arm_home_success(hass): + """Test arm home method success.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True + ) + + await hass.async_block_till_done() + assert STATE_ALARM_ARMED_HOME == hass.states.get(ENTITY_ID).state + + +async def test_arm_home_failure(hass): + """Test arm home method failure.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + with pytest.raises(Exception) as e: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{e.value}" == "TotalConnect failed to arm home test." + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + +async def test_arm_away_success(hass): + """Test arm away method success.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True + ) + await hass.async_block_till_done() + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state + + +async def test_arm_away_failure(hass): + """Test arm away method failure.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + with pytest.raises(Exception) as e: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{e.value}" == "TotalConnect failed to arm away test." + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + +async def test_disarm_success(hass): + """Test disarm method success.""" + responses = [RESPONSE_ARMED_AWAY, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True + ) + await hass.async_block_till_done() + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + +async def test_disarm_failure(hass): + """Test disarm method failure.""" + responses = [RESPONSE_ARMED_AWAY, RESPONSE_DISARM_FAILURE, RESPONSE_ARMED_AWAY] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state + + with pytest.raises(Exception) as e: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{e.value}" == "TotalConnect failed to disarm test." + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state