diff --git a/.coveragerc b/.coveragerc index 4e903d68aa5..aced69a4714 100644 --- a/.coveragerc +++ b/.coveragerc @@ -514,6 +514,7 @@ omit = homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* homeassistant/components/ialarm/alarm_control_panel.py + homeassistant/components/ialarm_xr/alarm_control_panel.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py diff --git a/.strict-typing b/.strict-typing index 2b6295b9157..e07d8b9cfc8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -127,6 +127,7 @@ homeassistant.components.homewizard.* homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* +homeassistant.components.ialarm_xr.* homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* diff --git a/CODEOWNERS b/CODEOWNERS index 9cc32503853..8a7a058e01f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -470,6 +470,8 @@ build.json @home-assistant/supervisor /tests/components/hyperion/ @dermotduffy /homeassistant/components/ialarm/ @RyuzakiKK /tests/components/ialarm/ @RyuzakiKK +/homeassistant/components/ialarm_xr/ @bigmoby +/tests/components/ialarm_xr/ @bigmoby /homeassistant/components/iammeter/ @lewei50 /homeassistant/components/iaqualink/ @flz /tests/components/iaqualink/ @flz diff --git a/homeassistant/components/ialarm_xr/__init__.py b/homeassistant/components/ialarm_xr/__init__.py new file mode 100644 index 00000000000..9a41b5ebab7 --- /dev/null +++ b/homeassistant/components/ialarm_xr/__init__.py @@ -0,0 +1,101 @@ +"""iAlarmXR integration.""" +from __future__ import annotations + +import asyncio +import logging + +from async_timeout import timeout +from pyialarmxr import ( + IAlarmXR, + IAlarmXRGenericException, + IAlarmXRSocketTimeoutException, +) + +from homeassistant.components.alarm_control_panel import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, IALARMXR_TO_HASS +from .utils import async_get_ialarmxr_mac + +PLATFORMS = [Platform.ALARM_CONTROL_PANEL] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up iAlarmXR config.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + + ialarmxr = IAlarmXR(username, password, host, port) + + try: + async with timeout(10): + ialarmxr_mac = await async_get_ialarmxr_mac(hass, ialarmxr) + except ( + asyncio.TimeoutError, + ConnectionError, + IAlarmXRGenericException, + IAlarmXRSocketTimeoutException, + ) as ex: + raise ConfigEntryNotReady from ex + + coordinator = IAlarmXRDataUpdateCoordinator(hass, ialarmxr, ialarmxr_mac) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload iAlarmXR config.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching iAlarmXR data.""" + + def __init__(self, hass: HomeAssistant, ialarmxr: IAlarmXR, mac: str) -> None: + """Initialize global iAlarm data updater.""" + self.ialarmxr: IAlarmXR = ialarmxr + self.state: str | None = None + self.host: str = ialarmxr.host + self.mac: str = mac + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + def _update_data(self) -> None: + """Fetch data from iAlarmXR via sync functions.""" + status: int = self.ialarmxr.get_status() + _LOGGER.debug("iAlarmXR status: %s", status) + + self.state = IALARMXR_TO_HASS.get(status) + + async def _async_update_data(self) -> None: + """Fetch data from iAlarmXR.""" + try: + async with timeout(10): + await self.hass.async_add_executor_job(self._update_data) + except ConnectionError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/ialarm_xr/alarm_control_panel.py b/homeassistant/components/ialarm_xr/alarm_control_panel.py new file mode 100644 index 00000000000..7b47ce3d7fa --- /dev/null +++ b/homeassistant/components/ialarm_xr/alarm_control_panel.py @@ -0,0 +1,63 @@ +"""Interfaces with iAlarmXR control panels.""" +from __future__ import annotations + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import IAlarmXRDataUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up a iAlarmXR alarm control panel based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([IAlarmXRPanel(coordinator)]) + + +class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity): + """Representation of an iAlarmXR device.""" + + _attr_supported_features = ( + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + ) + _attr_name = "iAlarm_XR" + _attr_icon = "mdi:security" + + def __init__(self, coordinator: IAlarmXRDataUpdateCoordinator) -> None: + """Initialize the alarm panel.""" + super().__init__(coordinator) + self.coordinator: IAlarmXRDataUpdateCoordinator = coordinator + self._attr_unique_id = coordinator.mac + self._attr_device_info = DeviceInfo( + manufacturer="Antifurto365 - Meian", + name=self.name, + connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)}, + ) + + @property + def state(self) -> str | None: + """Return the state of the device.""" + return self.coordinator.state + + def alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + self.coordinator.ialarmxr.disarm() + + def alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + self.coordinator.ialarmxr.arm_stay() + + def alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + self.coordinator.ialarmxr.arm_away() diff --git a/homeassistant/components/ialarm_xr/config_flow.py b/homeassistant/components/ialarm_xr/config_flow.py new file mode 100644 index 00000000000..06509a82eb5 --- /dev/null +++ b/homeassistant/components/ialarm_xr/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for Antifurto365 iAlarmXR integration.""" +from __future__ import annotations + +import logging +from logging import Logger +from typing import Any + +from pyialarmxr import ( + IAlarmXR, + IAlarmXRGenericException, + IAlarmXRSocketTimeoutException, +) +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN +from .utils import async_get_ialarmxr_mac + +_LOGGER: Logger = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=IAlarmXR.IALARM_P2P_DEFAULT_HOST): str, + vol.Required(CONF_PORT, default=IAlarmXR.IALARM_P2P_DEFAULT_PORT): int, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +async def _async_get_device_formatted_mac( + hass: core.HomeAssistant, username: str, password: str, host: str, port: int +) -> str: + """Return iAlarmXR mac address.""" + + ialarmxr = IAlarmXR(username, password, host, port) + return await async_get_ialarmxr_mac(hass, ialarmxr) + + +class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Antifurto365 iAlarmXR.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + + errors = {} + + if user_input is not None: + mac = None + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + try: + # If we are able to get the MAC address, we are able to establish + # a connection to the device. + mac = await _async_get_device_formatted_mac( + self.hass, username, password, host, port + ) + except ConnectionError: + errors["base"] = "cannot_connect" + except IAlarmXRGenericException as ialarmxr_exception: + _LOGGER.debug( + "IAlarmXRGenericException with message: [ %s ]", + ialarmxr_exception.message, + ) + errors["base"] = "unknown" + except IAlarmXRSocketTimeoutException as ialarmxr_socket_timeout_exception: + _LOGGER.debug( + "IAlarmXRSocketTimeoutException with message: [ %s ]", + ialarmxr_socket_timeout_exception.message, + ) + errors["base"] = "unknown" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_HOST], data=user_input + ) + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/ialarm_xr/const.py b/homeassistant/components/ialarm_xr/const.py new file mode 100644 index 00000000000..a208f5290b6 --- /dev/null +++ b/homeassistant/components/ialarm_xr/const.py @@ -0,0 +1,18 @@ +"""Constants for the iAlarmXR integration.""" +from pyialarmxr import IAlarmXR + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) + +DOMAIN = "ialarm_xr" + +IALARMXR_TO_HASS = { + IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME, + IAlarmXR.DISARMED: STATE_ALARM_DISARMED, + IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED, +} diff --git a/homeassistant/components/ialarm_xr/manifest.json b/homeassistant/components/ialarm_xr/manifest.json new file mode 100644 index 00000000000..4861e9c901f --- /dev/null +++ b/homeassistant/components/ialarm_xr/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "ialarm_xr", + "name": "Antifurto365 iAlarmXR", + "documentation": "https://www.home-assistant.io/integrations/ialarmxr", + "requirements": ["pyialarmxr==1.0.13"], + "codeowners": ["@bigmoby"], + "config_flow": true, + "iot_class": "cloud_polling", + "loggers": ["pyialarmxr"] +} diff --git a/homeassistant/components/ialarm_xr/strings.json b/homeassistant/components/ialarm_xr/strings.json new file mode 100644 index 00000000000..1650ae28c84 --- /dev/null +++ b/homeassistant/components/ialarm_xr/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/ialarm_xr/translations/en.json b/homeassistant/components/ialarm_xr/translations/en.json new file mode 100644 index 00000000000..bf2bf989dcd --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/utils.py b/homeassistant/components/ialarm_xr/utils.py new file mode 100644 index 00000000000..db82a3fcd44 --- /dev/null +++ b/homeassistant/components/ialarm_xr/utils.py @@ -0,0 +1,18 @@ +"""iAlarmXR utils.""" +import logging + +from pyialarmxr import IAlarmXR + +from homeassistant import core +from homeassistant.helpers.device_registry import format_mac + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_ialarmxr_mac(hass: core.HomeAssistant, ialarmxr: IAlarmXR) -> str: + """Retrieve iAlarmXR MAC address.""" + _LOGGER.debug("Retrieving ialarmxr mac address") + + mac = await hass.async_add_executor_job(ialarmxr.get_mac) + + return format_mac(mac) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b0bbd3e1b36..e9ba5971e07 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -161,6 +161,7 @@ FLOWS = { "hvv_departures", "hyperion", "ialarm", + "ialarm_xr", "iaqualink", "icloud", "ifttt", diff --git a/mypy.ini b/mypy.ini index 3cc9653e27a..e0c512782fb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1160,6 +1160,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ialarm_xr.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.image_processing.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index a52d465fac6..0f11d8576bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1549,6 +1549,9 @@ pyhomeworks==0.0.6 # homeassistant.components.ialarm pyialarm==1.9.0 +# homeassistant.components.ialarm_xr +pyialarmxr==1.0.13 + # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 897ffe50074..f13a1a87dda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,6 +1037,9 @@ pyhomematic==0.1.77 # homeassistant.components.ialarm pyialarm==1.9.0 +# homeassistant.components.ialarm_xr +pyialarmxr==1.0.13 + # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/tests/components/ialarm_xr/__init__.py b/tests/components/ialarm_xr/__init__.py new file mode 100644 index 00000000000..4097867f70b --- /dev/null +++ b/tests/components/ialarm_xr/__init__.py @@ -0,0 +1 @@ +"""Tests for the Antifurto365 iAlarmXR integration.""" diff --git a/tests/components/ialarm_xr/test_config_flow.py b/tests/components/ialarm_xr/test_config_flow.py new file mode 100644 index 00000000000..22a70bda067 --- /dev/null +++ b/tests/components/ialarm_xr/test_config_flow.py @@ -0,0 +1,185 @@ +"""Test the Antifurto365 iAlarmXR config flow.""" + +from unittest.mock import patch + +from pyialarmxr import IAlarmXRGenericException, IAlarmXRSocketTimeoutException + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.ialarm_xr.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME + +from tests.common import MockConfigEntry + +TEST_DATA = { + CONF_HOST: "1.1.1.1", + CONF_PORT: 18034, + CONF_USERNAME: "000ZZZ0Z00", + CONF_PASSWORD: "00000000", +} + +TEST_MAC = "00:00:54:12:34:56" + + +async def test_form(hass): + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["handler"] == "ialarm_xr" + assert result["data_schema"].schema.get("host") == str + assert result["data_schema"].schema.get("port") == int + assert result["data_schema"].schema.get("password") == str + assert result["data_schema"].schema.get("username") == str + assert result["errors"] == {} + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_status", + return_value=1, + ), patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + return_value=TEST_MAC, + ), patch( + "homeassistant.components.ialarm_xr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == TEST_DATA["host"] + assert result2["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_exception(hass): + """Test we handle unknown exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect_throwing_connection_error(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_cannot_connect_throwing_socket_timeout_exception(hass): + """Test we handle cannot connect error because of socket timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=IAlarmXRSocketTimeoutException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect_throwing_generic_exception(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=IAlarmXRGenericException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_already_exists(hass): + """Test that a flow with an existing host aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_MAC, + data=TEST_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + return_value=TEST_MAC, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_flow_user_step_no_input(hass): + """Test appropriate error when no input is provided.""" + _result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + _result["flow_id"], user_input=None + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + assert result["errors"] == {} diff --git a/tests/components/ialarm_xr/test_init.py b/tests/components/ialarm_xr/test_init.py new file mode 100644 index 00000000000..8486b7049e6 --- /dev/null +++ b/tests/components/ialarm_xr/test_init.py @@ -0,0 +1,120 @@ +"""Test the Antifurto365 iAlarmXR init.""" +import asyncio +from datetime import timedelta +from unittest.mock import Mock, patch +from uuid import uuid4 + +import pytest + +from homeassistant.components.ialarm_xr.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture(name="ialarmxr_api") +def ialarmxr_api_fixture(): + """Set up IAlarmXR API fixture.""" + with patch("homeassistant.components.ialarm_xr.IAlarmXR") as mock_ialarm_api: + yield mock_ialarm_api + + +@pytest.fixture(name="mock_config_entry") +def mock_config_fixture(): + """Return a fake config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.10.20", + CONF_PORT: 18034, + CONF_USERNAME: "000ZZZ0Z00", + CONF_PASSWORD: "00000000", + }, + entry_id=str(uuid4()), + ) + + +async def test_setup_entry(hass, ialarmxr_api, mock_config_entry): + """Test setup entry.""" + ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + ialarmxr_api.return_value.get_mac.assert_called_once() + assert mock_config_entry.state is ConfigEntryState.LOADED + + +async def test_setup_not_ready(hass, ialarmxr_api, mock_config_entry): + """Test setup failed because we can't connect to the alarm system.""" + ialarmxr_api.return_value.get_mac = Mock(side_effect=ConnectionError) + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass, ialarmxr_api, mock_config_entry): + """Test being able to unload an entry.""" + ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_setup_not_ready_connection_error(hass, ialarmxr_api, mock_config_entry): + """Test setup failed because we can't connect to the alarm system.""" + ialarmxr_api.return_value.get_status = Mock(side_effect=ConnectionError) + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + future = utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_not_ready_timeout(hass, ialarmxr_api, mock_config_entry): + """Test setup failed because we can't connect to the alarm system.""" + ialarmxr_api.return_value.get_status = Mock(side_effect=asyncio.TimeoutError) + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + future = utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_entry_and_then_fail_on_update( + hass, ialarmxr_api, mock_config_entry +): + """Test setup entry.""" + ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") + ialarmxr_api.return_value.get_status = Mock(value=ialarmxr_api.DISARMED) + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + ialarmxr_api.return_value.get_mac.assert_called_once() + ialarmxr_api.return_value.get_status.assert_called_once() + assert mock_config_entry.state is ConfigEntryState.LOADED + + ialarmxr_api.return_value.get_status = Mock(side_effect=asyncio.TimeoutError) + future = utcnow() + timedelta(seconds=60) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + ialarmxr_api.return_value.get_status.assert_called_once() + assert hass.states.get("alarm_control_panel.ialarm_xr").state == "unavailable"