Add tests to concord232 component (#156070)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Neal
2025-11-21 06:13:08 -06:00
committed by GitHub
parent 7c1b8ee02c
commit ac46568996
5 changed files with 518 additions and 0 deletions
+3
View File
@@ -649,6 +649,9 @@ colorthief==0.2.1
# homeassistant.components.compit
compit-inext-api==0.3.1
# homeassistant.components.concord232
concord232==0.15.1
# homeassistant.components.xiaomi_miio
construct==2.10.68
+1
View File
@@ -0,0 +1 @@
"""Tests for the Concord232 integration."""
+33
View File
@@ -0,0 +1,33 @@
"""Fixtures for the Concord232 integration."""
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def mock_concord232_client() -> Generator[MagicMock]:
"""Mock the concord232 Client for easier testing."""
with (
patch(
"homeassistant.components.concord232.alarm_control_panel.concord232_client.Client",
autospec=True,
) as mock_client_class,
patch(
"homeassistant.components.concord232.binary_sensor.concord232_client.Client",
new=mock_client_class,
),
):
mock_instance = mock_client_class.return_value
# Set up default return values
mock_instance.list_partitions.return_value = [{"arming_level": "Off"}]
mock_instance.list_zones.return_value = [
{"number": 1, "name": "Zone 1", "state": "Normal"},
{"number": 2, "name": "Zone 2", "state": "Normal"},
]
yield mock_instance
@@ -0,0 +1,280 @@
"""Tests for the Concord232 alarm control panel platform."""
from __future__ import annotations
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
import pytest
import requests
from homeassistant.components.alarm_control_panel import (
DOMAIN as ALARM_DOMAIN,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_DISARM,
AlarmControlPanelState,
)
from homeassistant.const import (
ATTR_CODE,
ATTR_ENTITY_ID,
CONF_CODE,
CONF_HOST,
CONF_MODE,
CONF_NAME,
CONF_PORT,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import async_fire_time_changed
VALID_CONFIG = {
ALARM_DOMAIN: {
"platform": "concord232",
CONF_HOST: "localhost",
CONF_PORT: 5007,
CONF_NAME: "Test Alarm",
}
}
VALID_CONFIG_WITH_CODE = {
ALARM_DOMAIN: {
"platform": "concord232",
CONF_HOST: "localhost",
CONF_PORT: 5007,
CONF_NAME: "Test Alarm",
CONF_CODE: "1234",
}
}
VALID_CONFIG_SILENT_MODE = {
ALARM_DOMAIN: {
"platform": "concord232",
CONF_HOST: "localhost",
CONF_PORT: 5007,
CONF_NAME: "Test Alarm",
CONF_MODE: "silent",
}
}
async def test_setup_platform(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test platform setup."""
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
state = hass.states.get("alarm_control_panel.test_alarm")
assert state is not None
assert state.state == AlarmControlPanelState.DISARMED
async def test_setup_platform_connection_error(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test platform setup with connection error."""
mock_concord232_client.list_partitions.side_effect = (
requests.exceptions.ConnectionError("Connection failed")
)
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
assert hass.states.get("alarm_control_panel.test_alarm") is None
async def test_alarm_disarm(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test disarm service."""
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
await hass.services.async_call(
ALARM_DOMAIN,
SERVICE_ALARM_DISARM,
{ATTR_ENTITY_ID: "alarm_control_panel.test_alarm"},
blocking=True,
)
mock_concord232_client.disarm.assert_called_once_with(None)
async def test_alarm_disarm_with_code(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test disarm service with code."""
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG_WITH_CODE)
await hass.async_block_till_done()
await hass.services.async_call(
ALARM_DOMAIN,
SERVICE_ALARM_DISARM,
{
ATTR_ENTITY_ID: "alarm_control_panel.test_alarm",
ATTR_CODE: "1234",
},
blocking=True,
)
mock_concord232_client.disarm.assert_called_once_with("1234")
async def test_alarm_disarm_invalid_code(
hass: HomeAssistant,
mock_concord232_client: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test disarm service with invalid code."""
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG_WITH_CODE)
await hass.async_block_till_done()
await hass.services.async_call(
ALARM_DOMAIN,
SERVICE_ALARM_DISARM,
{
ATTR_ENTITY_ID: "alarm_control_panel.test_alarm",
ATTR_CODE: "9999",
},
blocking=True,
)
mock_concord232_client.disarm.assert_not_called()
assert "Invalid code given" in caplog.text
@pytest.mark.parametrize(
("service", "expected_arm_call"),
[
(SERVICE_ALARM_ARM_HOME, "stay"),
(SERVICE_ALARM_ARM_AWAY, "away"),
],
)
async def test_alarm_arm(
hass: HomeAssistant,
mock_concord232_client: MagicMock,
service: str,
expected_arm_call: str,
) -> None:
"""Test arm service."""
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG_WITH_CODE)
await hass.async_block_till_done()
await hass.services.async_call(
ALARM_DOMAIN,
service,
{
ATTR_ENTITY_ID: "alarm_control_panel.test_alarm",
ATTR_CODE: "1234",
},
blocking=True,
)
mock_concord232_client.arm.assert_called_once_with(expected_arm_call)
async def test_alarm_arm_home_silent_mode(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test arm home service with silent mode."""
config_with_code = VALID_CONFIG_SILENT_MODE.copy()
config_with_code[ALARM_DOMAIN][CONF_CODE] = "1234"
await async_setup_component(hass, ALARM_DOMAIN, config_with_code)
await hass.async_block_till_done()
await hass.services.async_call(
ALARM_DOMAIN,
SERVICE_ALARM_ARM_HOME,
{
ATTR_ENTITY_ID: "alarm_control_panel.test_alarm",
ATTR_CODE: "1234",
},
blocking=True,
)
mock_concord232_client.arm.assert_called_once_with("stay", "silent")
async def test_update_state_disarmed(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test update when alarm is disarmed."""
mock_concord232_client.list_partitions.return_value = [{"arming_level": "Off"}]
mock_concord232_client.list_zones.return_value = [
{"number": 1, "name": "Zone 1", "state": "Normal"},
]
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
state = hass.states.get("alarm_control_panel.test_alarm")
assert state.state == AlarmControlPanelState.DISARMED
@pytest.mark.parametrize(
("arming_level", "expected_state"),
[
("Home", AlarmControlPanelState.ARMED_HOME),
("Away", AlarmControlPanelState.ARMED_AWAY),
],
)
async def test_update_state_armed(
hass: HomeAssistant,
mock_concord232_client: MagicMock,
freezer: FrozenDateTimeFactory,
arming_level: str,
expected_state: str,
) -> None:
"""Test update when alarm is armed."""
mock_concord232_client.list_partitions.return_value = [
{"arming_level": arming_level}
]
mock_concord232_client.partitions = (
mock_concord232_client.list_partitions.return_value
)
mock_concord232_client.list_zones.return_value = [
{"number": 1, "name": "Zone 1", "state": "Normal"},
]
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
# Trigger update
freezer.tick(10)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("alarm_control_panel.test_alarm")
assert state.state == expected_state
async def test_update_connection_error(
hass: HomeAssistant,
mock_concord232_client: MagicMock,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test update with connection error."""
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
mock_concord232_client.list_partitions.side_effect = (
requests.exceptions.ConnectionError("Connection failed")
)
freezer.tick(10)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert "Unable to connect to" in caplog.text
async def test_update_no_partitions(
hass: HomeAssistant,
mock_concord232_client: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test update when no partitions are available."""
mock_concord232_client.list_partitions.return_value = []
await async_setup_component(hass, ALARM_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
assert "Concord232 reports no partitions" in caplog.text
@@ -0,0 +1,201 @@
"""Tests for the Concord232 binary sensor platform."""
from __future__ import annotations
import datetime
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
import pytest
import requests
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.components.concord232.binary_sensor import (
CONF_EXCLUDE_ZONES,
CONF_ZONE_TYPES,
)
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import async_fire_time_changed
VALID_CONFIG = {
BINARY_SENSOR_DOMAIN: {
"platform": "concord232",
CONF_HOST: "localhost",
CONF_PORT: 5007,
}
}
VALID_CONFIG_WITH_EXCLUDE = {
BINARY_SENSOR_DOMAIN: {
"platform": "concord232",
CONF_HOST: "localhost",
CONF_PORT: 5007,
CONF_EXCLUDE_ZONES: [2],
}
}
VALID_CONFIG_WITH_ZONE_TYPES = {
BINARY_SENSOR_DOMAIN: {
"platform": "concord232",
CONF_HOST: "localhost",
CONF_PORT: 5007,
CONF_ZONE_TYPES: {1: "door", 2: "window"},
}
}
async def test_setup_platform(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test platform setup."""
await async_setup_component(hass, BINARY_SENSOR_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
state1 = hass.states.get("binary_sensor.zone_1")
state2 = hass.states.get("binary_sensor.zone_2")
assert state1 is not None
assert state2 is not None
assert state1.state == "off"
assert state2.state == "off"
async def test_setup_platform_connection_error(
hass: HomeAssistant,
mock_concord232_client: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test platform setup with connection error."""
mock_concord232_client.list_zones.side_effect = requests.exceptions.ConnectionError(
"Connection failed"
)
await async_setup_component(hass, BINARY_SENSOR_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
assert "Unable to connect to Concord232" in caplog.text
assert hass.states.get("binary_sensor.zone_1") is None
async def test_setup_with_exclude_zones(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test platform setup with excluded zones."""
await async_setup_component(hass, BINARY_SENSOR_DOMAIN, VALID_CONFIG_WITH_EXCLUDE)
await hass.async_block_till_done()
state1 = hass.states.get("binary_sensor.zone_1")
state2 = hass.states.get("binary_sensor.zone_2")
assert state1 is not None
assert state2 is None # Zone 2 should be excluded
async def test_setup_with_zone_types(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test platform setup with custom zone types."""
await async_setup_component(
hass, BINARY_SENSOR_DOMAIN, VALID_CONFIG_WITH_ZONE_TYPES
)
await hass.async_block_till_done()
state1 = hass.states.get("binary_sensor.zone_1")
state2 = hass.states.get("binary_sensor.zone_2")
assert state1 is not None
assert state2 is not None
# Check device class is set correctly
assert state1.attributes.get("device_class") == BinarySensorDeviceClass.DOOR
assert state2.attributes.get("device_class") == BinarySensorDeviceClass.WINDOW
async def test_zone_state_faulted(
hass: HomeAssistant, mock_concord232_client: MagicMock
) -> None:
"""Test zone state when faulted."""
mock_concord232_client.list_zones.return_value = [
{"number": 1, "name": "Zone 1", "state": "Faulted"},
]
mock_concord232_client.zones = mock_concord232_client.list_zones.return_value
await async_setup_component(hass, BINARY_SENSOR_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.zone_1")
assert state.state == "on" # Faulted state means on (faulted)
@pytest.mark.freeze_time("2023-10-21")
async def test_zone_update_refresh(
hass: HomeAssistant,
mock_concord232_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that zone updates refresh the client data."""
mock_concord232_client.list_zones.return_value = [
{"number": 1, "name": "Zone 1", "state": "Normal"},
]
mock_concord232_client.zones = mock_concord232_client.list_zones.return_value
await async_setup_component(hass, BINARY_SENSOR_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.zone_1").state == "off"
# Update zone state - need to update both return_value and zones attribute
new_zones = [
{"number": 1, "name": "Zone 1", "state": "Faulted"},
]
mock_concord232_client.list_zones.return_value = new_zones
mock_concord232_client.zones = new_zones
freezer.tick(datetime.timedelta(seconds=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
freezer.tick(datetime.timedelta(seconds=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.zone_1")
assert state.state == "on"
@pytest.mark.parametrize(
("sensor_name", "entity_id", "expected_device_class"),
[
(
"MOTION Sensor",
"binary_sensor.motion_sensor",
BinarySensorDeviceClass.MOTION,
),
("SMOKE Sensor", "binary_sensor.smoke_sensor", BinarySensorDeviceClass.SMOKE),
(
"Unknown Sensor",
"binary_sensor.unknown_sensor",
BinarySensorDeviceClass.OPENING,
),
],
)
async def test_device_class(
hass: HomeAssistant,
mock_concord232_client: MagicMock,
sensor_name: str,
entity_id: str,
expected_device_class: BinarySensorDeviceClass,
) -> None:
"""Test zone type detection for motion sensor."""
mock_concord232_client.list_zones.return_value = [
{"number": 1, "name": sensor_name, "state": "Normal"},
]
mock_concord232_client.zones = mock_concord232_client.list_zones.return_value
await async_setup_component(hass, BINARY_SENSOR_DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.attributes.get("device_class") == expected_device_class