Skip to content

Commit

Permalink
feat: add AWAY state configuration (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
palazzem authored Nov 8, 2023
1 parent becc4db commit 6c4e308
Show file tree
Hide file tree
Showing 12 changed files with 103 additions and 30 deletions.
2 changes: 1 addition & 1 deletion custom_components/econnect_metronet/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ async def async_alarm_disarm(self, code=None):
@set_device_state(STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMING)
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
await self.hass.async_add_executor_job(self._device.arm, code)
await self.hass.async_add_executor_job(self._device.arm, code, self._device._sectors_away)

@set_device_state(STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMING)
async def async_alarm_arm_home(self, code=None):
Expand Down
8 changes: 7 additions & 1 deletion custom_components/econnect_metronet/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from requests.exceptions import ConnectionError, HTTPError

from .const import (
CONF_AREAS_ARM_AWAY,
CONF_AREAS_ARM_HOME,
CONF_AREAS_ARM_NIGHT,
CONF_AREAS_ARM_VACATION,
Expand Down Expand Up @@ -104,7 +105,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
"""Reconfigure integration options.
Available options are:
* Areas armed in Arm Away state
* Areas armed in Arm Away state. If not set all sectors are armed.
* Areas armed in Arm Home state
* Areas armed in Arm Night state
* Areas armed in Arm Vacation state
"""
Expand All @@ -131,6 +133,10 @@ async def async_step_init(self, user_input=None):
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_AREAS_ARM_AWAY,
default=self.config_entry.options.get(CONF_AREAS_ARM_AWAY, []),
): select(sectors),
vol.Optional(
CONF_AREAS_ARM_HOME,
default=self.config_entry.options.get(CONF_AREAS_ARM_HOME, []),
Expand Down
1 change: 1 addition & 0 deletions custom_components/econnect_metronet/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CONF_DOMAIN = "domain"
CONF_SYSTEM_URL = "system_base_url"
CONF_SYSTEM_NAME = "system_name"
CONF_AREAS_ARM_AWAY = "areas_arm_away"
CONF_AREAS_ARM_HOME = "areas_arm_home"
CONF_AREAS_ARM_NIGHT = "areas_arm_night"
CONF_AREAS_ARM_VACATION = "areas_arm_vacation"
Expand Down
8 changes: 7 additions & 1 deletion custom_components/econnect_metronet/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
)
from requests.exceptions import HTTPError

from .const import CONF_AREAS_ARM_HOME, CONF_AREAS_ARM_NIGHT, CONF_AREAS_ARM_VACATION
from .const import (
CONF_AREAS_ARM_AWAY,
CONF_AREAS_ARM_HOME,
CONF_AREAS_ARM_NIGHT,
CONF_AREAS_ARM_VACATION,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,6 +50,7 @@ def __init__(self, connection, config=None):

# Load user configuration
config = config or {}
self._sectors_away = config.get(CONF_AREAS_ARM_AWAY) or []
self._sectors_home = config.get(CONF_AREAS_ARM_HOME) or []
self._sectors_night = config.get(CONF_AREAS_ARM_NIGHT) or []
self._sectors_vacation = config.get(CONF_AREAS_ARM_VACATION) or []
Expand Down
11 changes: 6 additions & 5 deletions custom_components/econnect_metronet/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@
"step": {
"init": {
"data": {
"areas_arm_home": "Armed areas while at home (e.g 3,4 - optional)",
"areas_arm_night": "Armed areas at night (e.g. 3,4 - optional)",
"areas_arm_vacation": "Armed areas when you are on vacation (e.g. 3,4 - optional)",
"areas_arm_away": "Armed areas while away (unset to arm all areas)",
"areas_arm_home": "Armed areas while at home (optional)",
"areas_arm_night": "Armed areas at night (optional)",
"areas_arm_vacation": "Armed areas when you are on vacation (optional)",
"scan_interval": "Scan interval (e.g. 120 - optional)"
},
"description": "Define sectors you want to arm in different modes.\n\nSet 'Scan Interval' value only if you want to reduce data usage, in case the system is connected through a mobile network. Leave it empty for real time updates, or set it to a value in seconds (e.g. 120 for one update every 2 minutes)",
"title": "Configure your Elmo/IESS system"
"description": "Define sectors you want to arm in different modes. If AWAY section is unset, all sectors are armed.\n\nSet 'Scan Interval' value only if you want to reduce data usage, in case the system is connected through a mobile network. Leave it empty for real time updates, or set it to a value in seconds (e.g. 120 for one update every 2 minutes)",
"title": "Configure your e-Connect/Metronet system"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion custom_components/econnect_metronet/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@
"step": {
"init": {
"data": {
"areas_arm_away": "Armed areas while away (unset to arm all areas)",
"areas_arm_home": "Armed areas while at home (optional)",
"areas_arm_night": "Armed areas at night (optional)",
"areas_arm_vacation": "Armed areas when you are on vacation (optional)",
"scan_interval": "Scan interval (e.g. 120 - optional)"
},
"description": "Define sectors you want to arm in different modes.\n\nSet 'Scan Interval' value only if you want to reduce data usage, in case the system is connected through a mobile network. Leave it empty for real time updates, or set it to a value in seconds (e.g. 120 for one update every 2 minutes)",
"description": "Define sectors you want to arm in different modes. If AWAY section is unset, all sectors are armed.\n\nSet 'Scan Interval' value only if you want to reduce data usage, in case the system is connected through a mobile network. Leave it empty for real time updates, or set it to a value in seconds (e.g. 120 for one update every 2 minutes)",
"title": "Configure your e-Connect/Metronet system"
}
}
Expand Down
3 changes: 2 additions & 1 deletion custom_components/econnect_metronet/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@
"step": {
"init": {
"data": {
"areas_arm_away": "Settori armati mentre sei fuori casa (attiva tutti i settori se non configurato)",
"areas_arm_home": "Settori armati mentre sei a casa (opzionale)",
"areas_arm_night": "Settori armati di notte (opzionale)",
"areas_arm_vacation": "Settori armati quando sei in vacanza (opzionale)",
"scan_interval": "Intervallo di scansione (es. 120 - opzionale)"
},
"description": "Scegli, tra quelli proposti, i settori che desideri armare nelle diverse modalità.\n\nImposta il valore 'Intervallo di scansione' solo se desideri ridurre l'utilizzo dei dati, nel caso in cui il sistema sia connesso tramite una rete mobile (SIM). Lascialo vuoto per aggiornamenti in tempo reale, oppure imposta un valore in secondi (es. 120 per un aggiornamento ogni 2 minuti)",
"description": "Scegli, tra quelli proposti, i settori che desideri armare nelle diverse modalità. Se l'opzione FUORI CASA non venisse configurata, tutti i settori saranno attivati in allarme.\n\nImposta il valore 'Intervallo di scansione' solo se desideri ridurre l'utilizzo dei dati, nel caso in cui il sistema sia connesso tramite una rete mobile (SIM). Lascialo vuoto per aggiornamenti in tempo reale, oppure imposta un valore in secondi (es. 120 per un aggiornamento ogni 2 minuti)",
"title": "Configura il tuo sistema e-Connect/Metronet"
}
}
Expand Down
11 changes: 3 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import logging

import pytest
import responses
from elmo.api.client import ElmoClient
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from custom_components.econnect_metronet import async_setup
from custom_components.econnect_metronet.alarm_control_panel import EconnectAlarm
Expand Down Expand Up @@ -53,7 +50,7 @@ def alarm_device(client):


@pytest.fixture(scope="function")
def alarm_entity(hass, client, config_entry):
def panel(hass, config_entry, alarm_device, coordinator):
"""Fixture to provide a test instance of the EconnectAlarm entity.
This sets up an AlarmDevice and its corresponding DataUpdateCoordinator,
Expand All @@ -67,11 +64,9 @@ def alarm_entity(hass, client, config_entry):
Yields:
EconnectAlarm: Initialized test instance of the EconnectAlarm entity.
"""
device = AlarmDevice(client)
coordinator = DataUpdateCoordinator(hass, logging.getLogger(__name__), name="econnect_metronet")
entity = EconnectAlarm(unique_id="test_id", config=config_entry, device=device, coordinator=coordinator)
entity = EconnectAlarm(unique_id="test_id", config=config_entry, device=alarm_device, coordinator=coordinator)
entity.hass = hass
yield entity
return entity


@pytest.fixture(scope="function")
Expand Down
22 changes: 22 additions & 0 deletions tests/test_alarm_panel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

import pytest
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from custom_components.econnect_metronet.alarm_control_panel import EconnectAlarm
Expand Down Expand Up @@ -38,3 +39,24 @@ def test_alarm_panel_entity_id_with_system_name(client, hass, config_entry):
coordinator = DataUpdateCoordinator(hass, logging.getLogger(__name__), name="econnect_metronet")
entity = EconnectAlarm("test_id", config_entry, device, coordinator)
assert entity.entity_id == "econnect_metronet.econnect_metronet_home"


@pytest.mark.asyncio
async def test_alarm_panel_arm_away(mocker, panel):
# Ensure an empty AWAY config arms all sectors
arm = mocker.patch.object(panel._device._connection, "arm", autopsec=True)
# Test
await panel.async_alarm_arm_away(code=42)
assert arm.call_count == 1
assert arm.call_args.kwargs["sectors"] == []


@pytest.mark.asyncio
async def test_alarm_panel_arm_away_with_options(mocker, panel):
# Ensure an empty AWAY config arms all sectors
arm = mocker.patch.object(panel._device._connection, "arm", autopsec=True)
panel._device._sectors_away = [1, 2]
# Test
await panel.async_alarm_arm_away(code=42)
assert arm.call_count == 1
assert arm.call_args.kwargs["sectors"] == [1, 2]
24 changes: 13 additions & 11 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,48 @@


@pytest.mark.asyncio
async def test_set_device_state_successful(alarm_entity):
async def test_set_device_state_successful(panel):
"""Should update the device state to the new state."""

@set_device_state("new_state", "loader_state")
async def test_func(self):
pass

# Test
await test_func(alarm_entity)
assert alarm_entity._device.state == "new_state"
await test_func(panel)
assert panel._device.state == "new_state"


@pytest.mark.asyncio
async def test_set_device_state_lock_error(alarm_entity):
async def test_set_device_state_lock_error(panel):
"""Should revert the device state to the previous state."""

@set_device_state("new_state", "loader_state")
async def test_func(self):
raise LockError()

panel._device.state = "old_state"
# Test
await test_func(alarm_entity)
assert alarm_entity._device.state == "unavailable"
await test_func(panel)
assert panel._device.state == "old_state"


@pytest.mark.asyncio
async def test_set_device_state_code_error(alarm_entity):
async def test_set_device_state_code_error(panel):
"""Should revert the device state to the previous state."""

@set_device_state("new_state", "loader_state")
async def test_func(self):
raise CodeError()

panel._device.state = "old_state"
# Test
await test_func(alarm_entity)
assert alarm_entity._device.state == "unavailable"
await test_func(panel)
assert panel._device.state == "old_state"


@pytest.mark.asyncio
async def test_set_device_state_loader_state(alarm_entity):
async def test_set_device_state_loader_state(panel):
"""Should use the loader_state until the function is completed."""

@set_device_state("new_state", "loader_state")
Expand All @@ -53,4 +55,4 @@ async def test_func(self):
assert self._device.state == "loader_state"

# Run test
await test_func(alarm_entity)
await test_func(panel)
25 changes: 25 additions & 0 deletions tests/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from requests.models import Response

from custom_components.econnect_metronet.const import (
CONF_AREAS_ARM_AWAY,
CONF_AREAS_ARM_HOME,
CONF_AREAS_ARM_NIGHT,
CONF_AREAS_ARM_VACATION,
Expand All @@ -27,6 +28,7 @@ def test_device_constructor(client):
assert device._connection == client
assert device._inventory == {}
assert device._last_ids == {10: 0, 9: 0, 11: 0}
assert device._sectors_away == []
assert device._sectors_home == []
assert device._sectors_night == []
assert device._sectors_vacation == []
Expand All @@ -36,6 +38,7 @@ def test_device_constructor(client):
def test_device_constructor_with_config(client):
"""Should initialize defaults attributes to run properly."""
config = {
CONF_AREAS_ARM_AWAY: [1, 2, 3, 4, 5],
CONF_AREAS_ARM_HOME: [3, 4],
CONF_AREAS_ARM_NIGHT: [1, 2, 3],
CONF_AREAS_ARM_VACATION: [5, 3],
Expand All @@ -45,6 +48,7 @@ def test_device_constructor_with_config(client):
assert device._connection == client
assert device._inventory == {}
assert device._last_ids == {10: 0, 9: 0, 11: 0}
assert device._sectors_away == [1, 2, 3, 4, 5]
assert device._sectors_home == [3, 4]
assert device._sectors_night == [1, 2, 3]
assert device._sectors_vacation == [5, 3]
Expand All @@ -54,6 +58,7 @@ def test_device_constructor_with_config(client):
def test_device_constructor_with_config_empty(client):
"""Should initialize defaults attributes to run properly."""
config = {
CONF_AREAS_ARM_AWAY: None,
CONF_AREAS_ARM_HOME: None,
CONF_AREAS_ARM_NIGHT: None,
CONF_AREAS_ARM_VACATION: None,
Expand All @@ -63,6 +68,7 @@ def test_device_constructor_with_config_empty(client):
assert device._connection == client
assert device._inventory == {}
assert device._last_ids == {10: 0, 9: 0, 11: 0}
assert device._sectors_away == []
assert device._sectors_home == []
assert device._sectors_night == []
assert device._sectors_vacation == []
Expand Down Expand Up @@ -871,6 +877,7 @@ def test_get_state_armed_vacation_out_of_order(alarm_device):

def test_get_state_armed_away(alarm_device):
"""Test when sectors are armed but don't match home or night."""
alarm_device._sectors_away = []
alarm_device._sectors_home = [1, 2, 3]
alarm_device._sectors_night = [4, 5, 6]
alarm_device._sectors_vacation = [4, 2]
Expand All @@ -887,6 +894,7 @@ def test_get_state_armed_away(alarm_device):

def test_get_state_armed_mixed(alarm_device):
"""Test when some sectors from home and night are armed."""
alarm_device._sectors_away = []
alarm_device._sectors_home = [1, 2, 3]
alarm_device._sectors_night = [4, 5, 6]
alarm_device._inventory = {
Expand All @@ -899,3 +907,20 @@ def test_get_state_armed_mixed(alarm_device):
}
# Test
assert alarm_device.get_state() == STATE_ALARM_ARMED_AWAY


def test_get_state_armed_away_with_config(alarm_device):
# Ensure arm AWAY is set when it matches the config value
alarm_device._sectors_away = [4]
alarm_device._sectors_home = [1, 2, 3]
alarm_device._sectors_night = [4, 5, 6]
alarm_device._sectors_vacation = [4, 2]
alarm_device._inventory = {
9: {
0: {"id": 1, "index": 0, "element": 1, "excluded": False, "status": False, "name": "S1 Living Room"},
1: {"id": 2, "index": 1, "element": 2, "excluded": False, "status": False, "name": "S2 Bedroom"},
2: {"id": 3, "index": 2, "element": 4, "excluded": False, "status": True, "name": "S3 Outdoor"},
}
}
# Test
assert alarm_device.get_state() == STATE_ALARM_ARMED_AWAY
15 changes: 14 additions & 1 deletion tests/test_options_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ async def test_form_fields(self, hass, config_entry):
assert form["step_id"] == "init"
assert form["errors"] == {}
assert list(form["data_schema"].schema.keys()) == [
"areas_arm_away",
"areas_arm_home",
"areas_arm_night",
"areas_arm_vacation",
"scan_interval",
]
assert isinstance(form["data_schema"].schema["areas_arm_away"], select)
assert isinstance(form["data_schema"].schema["areas_arm_home"], select)
assert isinstance(form["data_schema"].schema["areas_arm_night"], select)
assert isinstance(form["data_schema"].schema["areas_arm_vacation"], select)
Expand All @@ -49,7 +51,12 @@ async def test_form_submit_successful_empty(self, hass, config_entry):
# Check HA config
assert result["type"] == "create_entry"
assert result["title"] == "e-Connect/Metronet Alarm"
assert result["data"] == {"areas_arm_vacation": [], "areas_arm_home": [], "areas_arm_night": []}
assert result["data"] == {
"areas_arm_vacation": [],
"areas_arm_home": [],
"areas_arm_night": [],
"areas_arm_away": [],
}

async def test_form_submit_invalid_type(self, hass, config_entry):
# Ensure it fails if a user submits an option with an invalid type
Expand Down Expand Up @@ -102,6 +109,7 @@ async def test_form_submit_successful_with_identifier(self, hass, config_entry):
assert result["type"] == "create_entry"
assert result["title"] == "e-Connect/Metronet Alarm"
assert result["data"] == {
"areas_arm_away": [],
"areas_arm_home": [1],
"areas_arm_night": [],
"areas_arm_vacation": [],
Expand All @@ -127,6 +135,7 @@ async def test_form_submit_successful_with_input(self, hass, config_entry):
assert result["type"] == "create_entry"
assert result["title"] == "e-Connect/Metronet Alarm"
assert result["data"] == {
"areas_arm_away": [],
"areas_arm_home": [(1, "S1 Living Room")],
"areas_arm_night": [],
"areas_arm_vacation": [],
Expand All @@ -142,6 +151,9 @@ async def test_form_submit_successful_with_multiple_inputs(self, hass, config_en
result = await hass.config_entries.options.async_configure(
form["flow_id"],
user_input={
"areas_arm_away": [
(2, "S2 Bedroom"),
],
"areas_arm_home": [
(1, "S1 Living Room"),
],
Expand All @@ -156,6 +168,7 @@ async def test_form_submit_successful_with_multiple_inputs(self, hass, config_en
assert result["type"] == "create_entry"
assert result["title"] == "e-Connect/Metronet Alarm"
assert result["data"] == {
"areas_arm_away": [(2, "S2 Bedroom")],
"areas_arm_home": [(1, "S1 Living Room")],
"areas_arm_night": [(1, "S1 Living Room")],
"areas_arm_vacation": [(1, "S1 Living Room"), (2, "S2 Bedroom")],
Expand Down

0 comments on commit 6c4e308

Please sign in to comment.