Add Config Flow for Ness Alarm (#162414)

Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Joshua Leaper
2026-02-19 09:46:08 +10:30
committed by GitHub
parent 14b147b3f7
commit 0f874f7f03
14 changed files with 1724 additions and 245 deletions

View File

@@ -0,0 +1,104 @@
"""Test fixtures for ness_alarm."""
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.ness_alarm.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
from tests.common import MockConfigEntry
class MockClient:
"""Mock nessclient.Client stub."""
async def panic(self, code):
"""Handle panic."""
async def disarm(self, code):
"""Handle disarm."""
async def arm_away(self, code):
"""Handle arm_away."""
async def arm_home(self, code):
"""Handle arm_home."""
async def aux(self, output_id, state):
"""Handle auxiliary control."""
async def keepalive(self):
"""Handle keepalive."""
async def update(self):
"""Handle update."""
def on_zone_change(self):
"""Handle on_zone_change."""
def on_state_change(self):
"""Handle on_state_change."""
async def close(self):
"""Handle close."""
@pytest.fixture
def mock_nessclient():
"""Mock the nessclient Client constructor.
Replaces nessclient.Client with a Mock which always returns the same
MagicMock() instance.
"""
_mock_instance = MagicMock(MockClient())
_mock_factory = MagicMock()
_mock_factory.return_value = _mock_instance
with patch(
"homeassistant.components.ness_alarm.Client", new=_mock_factory, create=True
):
yield _mock_instance
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return a mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
@pytest.fixture
def mock_client() -> Generator[AsyncMock]:
"""Mock the nessclient Client for config flow tests."""
with patch(
"homeassistant.components.ness_alarm.config_flow.Client",
return_value=AsyncMock(),
) as mock:
yield mock.return_value
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Mock async_setup_entry."""
with patch(
"homeassistant.components.ness_alarm.async_setup_entry",
return_value=True,
) as mock:
yield mock
@pytest.fixture(autouse=True)
def post_connection_delay() -> Generator[None]:
"""Mock POST_CONNECTION_DELAY to 0 for faster tests."""
with patch(
"homeassistant.components.ness_alarm.config_flow.POST_CONNECTION_DELAY",
0,
):
yield

View File

@@ -0,0 +1,454 @@
"""Test the Ness Alarm config flow."""
from types import MappingProxyType
from unittest.mock import AsyncMock
import pytest
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.ness_alarm.const import (
CONF_INFER_ARMING_STATE,
CONF_SHOW_HOME_MODE,
CONF_ZONE_ID,
CONF_ZONE_NAME,
CONF_ZONE_NUMBER,
CONF_ZONE_TYPE,
CONF_ZONES,
DOMAIN,
SUBENTRY_TYPE_ZONE,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigSubentry
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
async def test_user_flow(
hass: HomeAssistant, mock_client: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test successful user config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
CONF_INFER_ARMING_STATE: False,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Ness Alarm 192.168.1.100:1992"
assert result["data"] == {
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
CONF_INFER_ARMING_STATE: False,
}
assert len(mock_setup_entry.mock_calls) == 1
mock_client.close.assert_awaited_once()
async def test_user_flow_with_infer_arming_state(
hass: HomeAssistant, mock_client: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test user flow with infer_arming_state enabled."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
CONF_INFER_ARMING_STATE: True,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"][CONF_INFER_ARMING_STATE] is True
async def test_user_flow_already_configured(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test we abort if already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
CONF_INFER_ARMING_STATE: False,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[
(OSError("Connection refused"), "cannot_connect"),
(TimeoutError, "cannot_connect"),
(RuntimeError("Unexpected"), "unknown"),
],
)
async def test_user_flow_connection_error_recovery(
hass: HomeAssistant,
mock_client: AsyncMock,
mock_setup_entry: AsyncMock,
side_effect: Exception,
expected_error: str,
) -> None:
"""Test connection error handling and recovery."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
# First attempt fails
mock_client.update.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
CONF_INFER_ARMING_STATE: False,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": expected_error}
mock_client.close.assert_awaited_once()
# Second attempt succeeds
mock_client.update.side_effect = None
mock_client.close.reset_mock()
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
CONF_INFER_ARMING_STATE: False,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_import_yaml_config(
hass: HomeAssistant, mock_client: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test importing YAML configuration."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: "192.168.1.72",
CONF_PORT: 4999,
CONF_INFER_ARMING_STATE: False,
CONF_ZONES: [
{CONF_ZONE_NAME: "Garage", CONF_ZONE_ID: 1},
{
CONF_ZONE_NAME: "Front Door",
CONF_ZONE_ID: 5,
CONF_ZONE_TYPE: BinarySensorDeviceClass.DOOR,
},
],
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Ness Alarm 192.168.1.72:4999"
assert result["data"] == {
CONF_HOST: "192.168.1.72",
CONF_PORT: 4999,
CONF_INFER_ARMING_STATE: False,
}
# Check that subentries were created for zones with names preserved
assert len(result["subentries"]) == 2
assert result["subentries"][0]["title"] == "Zone 1"
assert result["subentries"][0]["unique_id"] == "zone_1"
assert result["subentries"][0]["data"][CONF_TYPE] == BinarySensorDeviceClass.MOTION
assert result["subentries"][0]["data"][CONF_ZONE_NAME] == "Garage"
assert result["subentries"][1]["title"] == "Zone 5"
assert result["subentries"][1]["unique_id"] == "zone_5"
assert result["subentries"][1]["data"][CONF_TYPE] == BinarySensorDeviceClass.DOOR
assert result["subentries"][1]["data"][CONF_ZONE_NAME] == "Front Door"
assert len(mock_setup_entry.mock_calls) == 1
mock_client.close.assert_awaited_once()
@pytest.mark.parametrize(
("side_effect", "expected_reason"),
[
(OSError("Connection refused"), "cannot_connect"),
(TimeoutError, "cannot_connect"),
(RuntimeError("Unexpected"), "unknown"),
],
)
async def test_import_yaml_config_errors(
hass: HomeAssistant,
mock_client: AsyncMock,
mock_setup_entry: AsyncMock,
side_effect: Exception,
expected_reason: str,
) -> None:
"""Test importing YAML configuration."""
mock_client.update.side_effect = side_effect
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: "192.168.1.72",
CONF_PORT: 4999,
CONF_INFER_ARMING_STATE: False,
CONF_ZONES: [
{CONF_ZONE_NAME: "Garage", CONF_ZONE_ID: 1},
{
CONF_ZONE_NAME: "Front Door",
CONF_ZONE_ID: 5,
CONF_ZONE_TYPE: BinarySensorDeviceClass.DOOR,
},
],
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == expected_reason
async def test_import_already_configured(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test we abort import if already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 4999,
CONF_ZONES: [],
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
@pytest.mark.parametrize(
("side_effect", "expected_reason"),
[
(OSError("Connection refused"), "cannot_connect"),
(TimeoutError, "cannot_connect"),
(RuntimeError("Unexpected"), "unknown"),
],
)
async def test_import_connection_errors(
hass: HomeAssistant,
mock_client: AsyncMock,
side_effect: Exception,
expected_reason: str,
) -> None:
"""Test import aborts on connection errors."""
mock_client.update.side_effect = side_effect
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: "192.168.1.72",
CONF_PORT: 4999,
CONF_ZONES: [],
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == expected_reason
mock_client.close.assert_awaited_once()
async def test_zone_subentry_flow(hass: HomeAssistant) -> None:
"""Test adding a zone through subentry flow."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
result = await hass.config_entries.subentries.async_init(
(entry.entry_id, SUBENTRY_TYPE_ZONE),
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
CONF_ZONE_NUMBER: 1,
CONF_TYPE: BinarySensorDeviceClass.DOOR,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Zone 1"
assert result["data"][CONF_ZONE_NUMBER] == 1
assert result["data"][CONF_TYPE] == BinarySensorDeviceClass.DOOR
async def test_zone_subentry_already_configured(hass: HomeAssistant) -> None:
"""Test adding a zone that already exists."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
entry.subentries = {
"zone_1_id": ConfigSubentry(
subentry_type=SUBENTRY_TYPE_ZONE,
subentry_id="zone_1_id",
unique_id="zone_1",
title="Zone 1",
data=MappingProxyType(
{
CONF_ZONE_NUMBER: 1,
CONF_TYPE: BinarySensorDeviceClass.MOTION,
}
),
)
}
result = await hass.config_entries.subentries.async_init(
(entry.entry_id, SUBENTRY_TYPE_ZONE),
context={"source": SOURCE_USER},
)
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
CONF_ZONE_NUMBER: 1,
CONF_TYPE: BinarySensorDeviceClass.DOOR,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {CONF_ZONE_NUMBER: "already_configured"}
async def test_zone_subentry_reconfigure(hass: HomeAssistant) -> None:
"""Test reconfiguring an existing zone."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
zone_subentry = ConfigSubentry(
subentry_type=SUBENTRY_TYPE_ZONE,
subentry_id="zone_1_id",
unique_id="zone_1",
title="Zone 1",
data=MappingProxyType(
{
CONF_ZONE_NUMBER: 1,
CONF_TYPE: BinarySensorDeviceClass.MOTION,
}
),
)
entry.subentries = {"zone_1_id": zone_subentry}
result = await entry.start_subentry_reconfigure_flow(hass, "zone_1_id")
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
assert result["description_placeholders"][CONF_ZONE_NUMBER] == "1"
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
{
CONF_TYPE: BinarySensorDeviceClass.DOOR,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
async def test_options_flow(hass: HomeAssistant) -> None:
"""Test options flow to configure alarm panel settings."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{
CONF_SHOW_HOME_MODE: False,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert entry.options[CONF_SHOW_HOME_MODE] is False
async def test_options_flow_enable_home_mode(hass: HomeAssistant) -> None:
"""Test options flow to enable home mode."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
options={CONF_SHOW_HOME_MODE: False},
)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{
CONF_SHOW_HOME_MODE: True,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert entry.options[CONF_SHOW_HOME_MODE] is True

View File

@@ -1,26 +1,33 @@
"""Tests for the ness_alarm component."""
from unittest.mock import MagicMock, patch
from types import MappingProxyType
from unittest.mock import AsyncMock, patch
from nessclient import ArmingMode, ArmingState
import pytest
from homeassistant.components import alarm_control_panel
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.ness_alarm import (
ATTR_CODE,
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.ness_alarm.const import (
ATTR_OUTPUT_ID,
CONF_DEVICE_PORT,
CONF_ZONE_ID,
CONF_ZONE_NAME,
CONF_ZONES,
CONF_SHOW_HOME_MODE,
CONF_ZONE_NUMBER,
DOMAIN,
SERVICE_AUX,
SERVICE_PANIC,
SUBENTRY_TYPE_ZONE,
)
from homeassistant.config_entries import ConfigEntryState, ConfigSubentry
from homeassistant.const import (
ATTR_CODE,
ATTR_ENTITY_ID,
ATTR_STATE,
CONF_HOST,
CONF_PORT,
CONF_TYPE,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_DISARM,
@@ -28,70 +35,234 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
VALID_CONFIG = {
DOMAIN: {
CONF_HOST: "alarm.local",
CONF_DEVICE_PORT: 1234,
CONF_ZONES: [
{CONF_ZONE_NAME: "Zone 1", CONF_ZONE_ID: 1},
{CONF_ZONE_NAME: "Zone 2", CONF_ZONE_ID: 2},
],
}
}
from tests.common import MockConfigEntry
async def test_setup_platform(hass: HomeAssistant, mock_nessclient) -> None:
"""Test platform setup."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
assert hass.services.has_service(DOMAIN, "panic")
assert hass.services.has_service(DOMAIN, "aux")
async def test_config_entry_setup(hass: HomeAssistant, mock_nessclient) -> None:
"""Test config entry setup."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("alarm_control_panel.alarm_panel") is not None
assert hass.states.get("binary_sensor.zone_1") is not None
assert hass.states.get("binary_sensor.zone_2") is not None
# Services should be registered
assert hass.services.has_service(DOMAIN, SERVICE_PANIC)
assert hass.services.has_service(DOMAIN, SERVICE_AUX)
# Alarm panel should be created
assert hass.states.get("alarm_control_panel.alarm_panel")
# Client keepalive and update should be called after startup
assert mock_nessclient.keepalive.call_count == 1
assert mock_nessclient.update.call_count == 1
# update is called once during setup (connection test) and once after startup
assert mock_nessclient.update.call_count == 2
async def test_panic_service(hass: HomeAssistant, mock_nessclient) -> None:
"""Test calling panic service."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
async def test_config_entry_unload(hass: HomeAssistant, mock_nessclient) -> None:
"""Test config entry unload."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
# Client should be closed
mock_nessclient.close.assert_called_once()
async def test_config_entry_not_ready(hass: HomeAssistant, mock_nessclient) -> None:
"""Test config entry raises ConfigEntryNotReady on connection failure."""
mock_nessclient.update.side_effect = OSError("Connection refused")
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
mock_nessclient.close.assert_called_once()
async def test_config_entry_with_zones(hass: HomeAssistant, mock_nessclient) -> None:
"""Test config entry setup with zones as subentries."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
# Add zone subentries
entry.subentries = {
"zone_1_id": ConfigSubentry(
subentry_type=SUBENTRY_TYPE_ZONE,
subentry_id="zone_1_id",
unique_id="zone_1",
title="Zone 1",
data={
CONF_ZONE_NUMBER: 1,
CONF_TYPE: BinarySensorDeviceClass.MOTION,
},
),
"zone_2_id": ConfigSubentry(
subentry_type=SUBENTRY_TYPE_ZONE,
subentry_id="zone_2_id",
unique_id="zone_2",
title="Zone 2",
data={
CONF_ZONE_NUMBER: 2,
CONF_TYPE: BinarySensorDeviceClass.DOOR,
},
),
}
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Binary sensors should be created for each zone
assert hass.states.get("binary_sensor.zone_1")
assert hass.states.get("binary_sensor.zone_2")
async def test_config_entry_reload_on_subentry_add(
hass: HomeAssistant, mock_nessclient
) -> None:
"""Test config entry with subentries."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
# Add a zone subentry
entry.subentries = {
"zone_1_id": ConfigSubentry(
subentry_type=SUBENTRY_TYPE_ZONE,
subentry_id="zone_1_id",
unique_id="zone_1",
title="Zone 1",
data={
CONF_ZONE_NUMBER: 1,
CONF_TYPE: BinarySensorDeviceClass.MOTION,
},
),
}
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Zone entity should be created
assert hass.states.get("binary_sensor.zone_1")
async def test_panic_service_with_config_entry(
hass: HomeAssistant, mock_nessclient
) -> None:
"""Test calling panic service with config entry."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
DOMAIN, SERVICE_PANIC, blocking=True, service_data={ATTR_CODE: "1234"}
)
mock_nessclient.panic.assert_awaited_once_with("1234")
async def test_aux_service(hass: HomeAssistant, mock_nessclient) -> None:
"""Test calling aux service."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
async def test_aux_service_with_config_entry(
hass: HomeAssistant, mock_nessclient
) -> None:
"""Test calling aux service with config entry."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
DOMAIN, SERVICE_AUX, blocking=True, service_data={ATTR_OUTPUT_ID: 1}
)
mock_nessclient.aux.assert_awaited_once_with(1, True)
async def test_dispatch_state_change(hass: HomeAssistant, mock_nessclient) -> None:
"""Test calling aux service."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
on_state_change = mock_nessclient.on_state_change.call_args[0][0]
on_state_change(ArmingState.ARMING, None)
await hass.async_block_till_done()
assert hass.states.is_state(
"alarm_control_panel.alarm_panel", AlarmControlPanelState.ARMING
async def test_aux_service_with_state_false(
hass: HomeAssistant, mock_nessclient
) -> None:
"""Test calling aux service with state=False."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
DOMAIN,
SERVICE_AUX,
blocking=True,
service_data={ATTR_OUTPUT_ID: 2, ATTR_STATE: False},
)
mock_nessclient.aux.assert_awaited_once_with(2, False)
async def test_alarm_disarm(hass: HomeAssistant, mock_nessclient) -> None:
"""Test disarm."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
async def test_alarm_panel_disarm(hass: HomeAssistant, mock_nessclient) -> None:
"""Test alarm panel disarm."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
@@ -106,9 +277,17 @@ async def test_alarm_disarm(hass: HomeAssistant, mock_nessclient) -> None:
mock_nessclient.disarm.assert_called_once_with("1234")
async def test_alarm_arm_away(hass: HomeAssistant, mock_nessclient) -> None:
"""Test disarm."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
async def test_alarm_panel_arm_away(hass: HomeAssistant, mock_nessclient) -> None:
"""Test alarm panel arm away."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
@@ -123,9 +302,17 @@ async def test_alarm_arm_away(hass: HomeAssistant, mock_nessclient) -> None:
mock_nessclient.arm_away.assert_called_once_with("1234")
async def test_alarm_arm_home(hass: HomeAssistant, mock_nessclient) -> None:
"""Test disarm."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
async def test_alarm_panel_arm_home(hass: HomeAssistant, mock_nessclient) -> None:
"""Test alarm panel arm home."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
@@ -140,9 +327,17 @@ async def test_alarm_arm_home(hass: HomeAssistant, mock_nessclient) -> None:
mock_nessclient.arm_home.assert_called_once_with("1234")
async def test_alarm_trigger(hass: HomeAssistant, mock_nessclient) -> None:
"""Test disarm."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
async def test_alarm_panel_trigger(hass: HomeAssistant, mock_nessclient) -> None:
"""Test alarm panel trigger."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
@@ -157,21 +352,79 @@ async def test_alarm_trigger(hass: HomeAssistant, mock_nessclient) -> None:
mock_nessclient.panic.assert_called_once_with("1234")
async def test_dispatch_zone_change(hass: HomeAssistant, mock_nessclient) -> None:
"""Test zone change events dispatch a signal to subscribers."""
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
async def test_zone_state_change(hass: HomeAssistant, mock_nessclient) -> None:
"""Test zone state change events."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
# Add zone subentries
entry.subentries = {
"zone_1_id": ConfigSubentry(
subentry_type=SUBENTRY_TYPE_ZONE,
subentry_id="zone_1_id",
unique_id="zone_1",
title="Zone 1",
data={
CONF_ZONE_NUMBER: 1,
CONF_TYPE: BinarySensorDeviceClass.MOTION,
},
),
"zone_2_id": ConfigSubentry(
subentry_type=SUBENTRY_TYPE_ZONE,
subentry_id="zone_2_id",
unique_id="zone_2",
title="Zone 2",
data={
CONF_ZONE_NUMBER: 2,
CONF_TYPE: BinarySensorDeviceClass.DOOR,
},
),
}
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Get the zone change callback
on_zone_change = mock_nessclient.on_zone_change.call_args[0][0]
on_zone_change(1, True)
# Trigger zone 1
on_zone_change(1, True)
await hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.zone_1", "on")
assert hass.states.is_state("binary_sensor.zone_2", "off")
# Trigger zone 2
on_zone_change(2, True)
await hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.zone_2", "on")
# Clear zone 1
on_zone_change(1, False)
await hass.async_block_till_done()
assert hass.states.is_state("binary_sensor.zone_1", "off")
async def test_arming_state_change(hass: HomeAssistant, mock_nessclient) -> None:
"""Test arming state change handing."""
async def test_arming_state_changes(hass: HomeAssistant, mock_nessclient) -> None:
"""Test all arming state changes."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Get the state change callback
on_state_change = mock_nessclient.on_state_change.call_args[0][0]
states = [
(ArmingState.UNKNOWN, None, STATE_UNKNOWN),
(ArmingState.DISARMED, None, AlarmControlPanelState.DISARMED),
@@ -193,67 +446,185 @@ async def test_arming_state_change(hass: HomeAssistant, mock_nessclient) -> None
ArmingMode.ARMED_NIGHT,
AlarmControlPanelState.ARMED_NIGHT,
),
(
ArmingState.ARMED,
ArmingMode.ARMED_VACATION,
AlarmControlPanelState.ARMED_VACATION,
),
(
ArmingState.ARMED,
ArmingMode.ARMED_DAY,
AlarmControlPanelState.ARMED_AWAY,
),
(
ArmingState.ARMED,
ArmingMode.ARMED_HIGHEST,
AlarmControlPanelState.ARMED_AWAY,
),
(ArmingState.ENTRY_DELAY, None, AlarmControlPanelState.PENDING),
(ArmingState.TRIGGERED, None, AlarmControlPanelState.TRIGGERED),
]
await async_setup_component(hass, DOMAIN, VALID_CONFIG)
await hass.async_block_till_done()
assert hass.states.is_state("alarm_control_panel.alarm_panel", STATE_UNKNOWN)
on_state_change = mock_nessclient.on_state_change.call_args[0][0]
for arming_state, arming_mode, expected_state in states:
on_state_change(arming_state, arming_mode)
await hass.async_block_till_done()
assert hass.states.is_state("alarm_control_panel.alarm_panel", expected_state)
class MockClient:
"""Mock nessclient.Client stub."""
async def test_arming_state_unknown_mode(hass: HomeAssistant, mock_nessclient) -> None:
"""Test arming state with unknown arming mode (for coverage)."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
async def panic(self, code):
"""Handle panic."""
# Get the state change callback
on_state_change = mock_nessclient.on_state_change.call_args[0][0]
async def disarm(self, code):
"""Handle disarm."""
async def arm_away(self, code):
"""Handle arm_away."""
async def arm_home(self, code):
"""Handle arm_home."""
async def aux(self, output_id, state):
"""Handle auxiliary control."""
async def keepalive(self):
"""Handle keepalive."""
async def update(self):
"""Handle update."""
def on_zone_change(self):
"""Handle on_zone_change."""
def on_state_change(self):
"""Handle on_state_change."""
async def close(self):
"""Handle close."""
# Test with unhandled arming state (for coverage of warning log)
on_state_change(999, None) # Invalid state
await hass.async_block_till_done()
@pytest.fixture
def mock_nessclient():
"""Mock the nessclient Client constructor.
async def test_homeassistant_stop_event(hass: HomeAssistant, mock_nessclient) -> None:
"""Test client is closed on homeassistant_stop event."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
Replaces nessclient.Client with a Mock which always returns the same
MagicMock() instance.
"""
_mock_instance = MagicMock(MockClient())
_mock_factory = MagicMock()
_mock_factory.return_value = _mock_instance
# Fire the homeassistant_stop event
hass.bus.async_fire("homeassistant_stop")
await hass.async_block_till_done()
# Client should be closed
mock_nessclient.close.assert_called()
async def test_entry_reload_on_update(hass: HomeAssistant, mock_nessclient) -> None:
"""Test config entry reload when update listener is triggered."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Add a zone subentry which should trigger the update listener and reload
zone_subentry = ConfigSubentry(
subentry_type=SUBENTRY_TYPE_ZONE,
subentry_id="zone_1_id",
unique_id="zone_1",
title="Zone 1",
data=MappingProxyType(
{
CONF_ZONE_NUMBER: 1,
CONF_TYPE: BinarySensorDeviceClass.MOTION,
}
),
)
hass.config_entries.async_add_subentry(entry, zone_subentry)
await hass.async_block_till_done()
# Entry should have the new zone subentry
assert len(entry.subentries) == 1
async def test_alarm_panel_home_mode_disabled(
hass: HomeAssistant, mock_nessclient
) -> None:
"""Test alarm panel with home mode disabled via options."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
options={CONF_SHOW_HOME_MODE: False},
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("alarm_control_panel.alarm_panel")
assert state is not None
# ARM_HOME should not be in supported features
supported = state.attributes["supported_features"]
assert not supported & AlarmControlPanelEntityFeature.ARM_HOME
assert supported & AlarmControlPanelEntityFeature.ARM_AWAY
assert supported & AlarmControlPanelEntityFeature.TRIGGER
async def test_alarm_panel_home_mode_enabled_by_default(
hass: HomeAssistant, mock_nessclient
) -> None:
"""Test alarm panel has home mode enabled by default."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
},
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("alarm_control_panel.alarm_panel")
assert state is not None
# ARM_HOME should be in supported features by default
supported = state.attributes["supported_features"]
assert supported & AlarmControlPanelEntityFeature.ARM_HOME
assert supported & AlarmControlPanelEntityFeature.ARM_AWAY
assert supported & AlarmControlPanelEntityFeature.TRIGGER
async def test_yaml_import_triggers_flow(
hass: HomeAssistant, mock_setup_entry: AsyncMock, issue_registry: ir.IssueRegistry
) -> None:
"""Test that YAML configuration triggers import flow."""
with patch(
"homeassistant.components.ness_alarm.Client", new=_mock_factory, create=True
"homeassistant.components.ness_alarm.config_flow.Client",
return_value=AsyncMock(),
):
yield _mock_instance
config = {
DOMAIN: {
CONF_HOST: "192.168.1.100",
CONF_PORT: 1992,
}
}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
# Check that a config entry was created from the import
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].data[CONF_HOST] == "192.168.1.100"
assert entries[0].data[CONF_PORT] == 1992
# Check that a deprecation repair issue was created
issue = issue_registry.async_get_issue(
"homeassistant", f"deprecated_yaml_{DOMAIN}"
)
assert issue is not None
assert issue.severity == "warning"