Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions homeassistant/components/alarm_control_panel/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
Expand All @@ -32,6 +33,7 @@
TRIGGER_TYPES = {
"triggered",
"disarmed",
"arming",
"armed_home",
"armed_away",
"armed_night",
Expand Down Expand Up @@ -79,6 +81,13 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "triggered",
},
{
CONF_PLATFORM: "device",
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
CONF_ENTITY_ID: entry.entity_id,
CONF_TYPE: "arming",
},
]
if supported_features & SUPPORT_ALARM_ARM_HOME:
triggers.append(
Expand Down Expand Up @@ -122,29 +131,32 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Attach a trigger."""
config = TRIGGER_SCHEMA(config)
from_state = None

if config[CONF_TYPE] == "triggered":
from_state = STATE_ALARM_PENDING
to_state = STATE_ALARM_TRIGGERED
elif config[CONF_TYPE] == "disarmed":
Comment thread
starkillerOG marked this conversation as resolved.
from_state = STATE_ALARM_TRIGGERED
to_state = STATE_ALARM_DISARMED
elif config[CONF_TYPE] == "arming":
from_state = STATE_ALARM_DISARMED
to_state = STATE_ALARM_ARMING
elif config[CONF_TYPE] == "armed_home":
from_state = STATE_ALARM_PENDING
Comment thread
starkillerOG marked this conversation as resolved.
from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING
to_state = STATE_ALARM_ARMED_HOME
elif config[CONF_TYPE] == "armed_away":
from_state = STATE_ALARM_PENDING
from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING
to_state = STATE_ALARM_ARMED_AWAY
elif config[CONF_TYPE] == "armed_night":
from_state = STATE_ALARM_PENDING
from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING
to_state = STATE_ALARM_ARMED_NIGHT

state_config = {
state.CONF_PLATFORM: "state",
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
state.CONF_FROM: from_state,
state.CONF_TO: to_state,
}
if from_state:
state_config[state.CONF_FROM] = from_state
state_config = state.TRIGGER_SCHEMA(state_config)
return await state.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
Expand Down
12 changes: 6 additions & 6 deletions homeassistant/components/demo/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

from homeassistant.components.manual.alarm_control_panel import ManualAlarm
from homeassistant.const import (
CONF_ARMING_TIME,
CONF_DELAY_TIME,
CONF_PENDING_TIME,
CONF_TRIGGER_TIME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
Expand All @@ -28,31 +28,31 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
False,
{
STATE_ALARM_ARMED_AWAY: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_HOME: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_NIGHT: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_DISARMED: {
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
CONF_ARMING_TIME: datetime.timedelta(seconds=5),
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
},
STATE_ALARM_TRIGGERED: {
CONF_PENDING_TIME: datetime.timedelta(seconds=5)
CONF_ARMING_TIME: datetime.timedelta(seconds=5)
},
},
)
Expand Down
99 changes: 59 additions & 40 deletions homeassistant/components/manual/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,23 @@
SUPPORT_ALARM_TRIGGER,
)
from homeassistant.const import (
CONF_ARMING_TIME,
CONF_CODE,
CONF_DELAY_TIME,
CONF_DISARM_AFTER_TRIGGER,
CONF_NAME,
CONF_PENDING_TIME,
CONF_PLATFORM,
CONF_TRIGGER_TIME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time
from homeassistant.helpers.restore_state import RestoreEntity
Expand All @@ -41,8 +43,8 @@
CONF_CODE_ARM_REQUIRED = "code_arm_required"

DEFAULT_ALARM_NAME = "HA Alarm"
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=60)
Comment thread
starkillerOG marked this conversation as resolved.
DEFAULT_ARMING_TIME = datetime.timedelta(seconds=60)
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
DEFAULT_DISARM_AFTER_TRIGGER = False

Expand All @@ -59,12 +61,14 @@
state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
]

SUPPORTED_PENDING_STATES = [
state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
SUPPORTED_ARMING_STATES = [
state
for state in SUPPORTED_STATES
if state not in (STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
]

ATTR_PRE_PENDING_STATE = "pre_pending_state"
ATTR_POST_PENDING_STATE = "post_pending_state"
ATTR_PREVIOUS_STATE = "previous_state"
ATTR_NEXT_STATE = "next_state"


def _state_validator(config):
Expand All @@ -75,9 +79,9 @@ def _state_validator(config):
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
if CONF_TRIGGER_TIME not in config[state]:
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
for state in SUPPORTED_PENDING_STATES:
if CONF_PENDING_TIME not in config[state]:
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
for state in SUPPORTED_ARMING_STATES:
if CONF_ARMING_TIME not in config[state]:
config[state][CONF_ARMING_TIME] = config[CONF_ARMING_TIME]

return config

Expand All @@ -92,8 +96,8 @@ def _state_schema(state):
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta
)
if state in SUPPORTED_PENDING_STATES:
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
if state in SUPPORTED_ARMING_STATES:
schema[vol.Optional(CONF_ARMING_TIME)] = vol.All(
cv.time_period, cv.positive_timedelta
)
return vol.Schema(schema)
Expand All @@ -110,7 +114,7 @@ def _state_schema(state):
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
vol.Optional(CONF_ARMING_TIME, default=DEFAULT_ARMING_TIME): vol.All(
cv.time_period, cv.positive_timedelta
),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
Expand Down Expand Up @@ -164,9 +168,8 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity):
"""
Representation of an alarm status.

When armed, will be pending for 'pending_time', after that armed.
When triggered, will be pending for the triggering state's 'delay_time'
plus the triggered state's 'pending_time'.
When armed, will be arming for 'arming_time', after that armed.
When triggered, will be pending for the triggering state's 'delay_time'.
After that will be triggered for 'trigger_time', after that we return to
the previous state or disarm if `disarm_after_trigger` is true.
A trigger_time of zero disables the alarm_trigger service.
Expand Down Expand Up @@ -204,9 +207,8 @@ def __init__(
state: config[state][CONF_TRIGGER_TIME]
for state in SUPPORTED_PRETRIGGER_STATES
}
self._pending_time_by_state = {
state: config[state][CONF_PENDING_TIME]
for state in SUPPORTED_PENDING_STATES
self._arming_time_by_state = {
state: config[state][CONF_ARMING_TIME] for state in SUPPORTED_ARMING_STATES
}

@property
Expand Down Expand Up @@ -234,10 +236,10 @@ def state(self):
self._state = self._previous_state
return self._state

if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
if self._state in SUPPORTED_ARMING_STATES and self._within_arming_time(
self._state
):
return STATE_ALARM_PENDING
return STATE_ALARM_ARMING

return self._state

Expand All @@ -255,16 +257,21 @@ def supported_features(self) -> int:
@property
def _active_state(self):
"""Get the current state."""
if self.state == STATE_ALARM_PENDING:
if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING):
return self._previous_state
return self._state

def _arming_time(self, state):
"""Get the arming time."""
return self._arming_time_by_state[state]

def _pending_time(self, state):
"""Get the pending time."""
pending_time = self._pending_time_by_state[state]
if state == STATE_ALARM_TRIGGERED:
pending_time += self._delay_time_by_state[self._previous_state]
return pending_time
return self._delay_time_by_state[self._previous_state]

def _within_arming_time(self, state):
"""Get if the action is in the arming time window."""
return self._state_ts + self._arming_time(state) > dt_util.utcnow()

def _within_pending_time(self, state):
"""Get if the action is in the pending time window."""
Expand Down Expand Up @@ -350,22 +357,26 @@ def _update_state(self, state):
self._state_ts = dt_util.utcnow()
self.schedule_update_ha_state()

pending_time = self._pending_time(state)
if state == STATE_ALARM_TRIGGERED:
pending_time = self._pending_time(state)
track_point_in_time(
self._hass, self.async_update_ha_state, self._state_ts + pending_time
self._hass, self.async_scheduled_update, self._state_ts + pending_time
)

trigger_time = self._trigger_time_by_state[self._previous_state]
track_point_in_time(
self._hass,
self.async_update_ha_state,
self.async_scheduled_update,
self._state_ts + pending_time + trigger_time,
)
elif state in SUPPORTED_PENDING_STATES and pending_time:
track_point_in_time(
self._hass, self.async_update_ha_state, self._state_ts + pending_time
)
elif state in SUPPORTED_ARMING_STATES:
arming_time = self._arming_time(state)
if arming_time:
track_point_in_time(
self._hass,
self.async_scheduled_update,
self._state_ts + arming_time,
)

def _validate_code(self, code, state):
"""Validate given code."""
Expand All @@ -385,24 +396,32 @@ def device_state_attributes(self):
"""Return the state attributes."""
state_attr = {}

if self.state == STATE_ALARM_PENDING:
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
Comment thread
starkillerOG marked this conversation as resolved.
state_attr[ATTR_POST_PENDING_STATE] = self._state
if self.state == STATE_ALARM_PENDING or self.state == STATE_ALARM_ARMING:
state_attr[ATTR_PREVIOUS_STATE] = self._previous_state
state_attr[ATTR_NEXT_STATE] = self._state

return state_attr

@callback
def async_scheduled_update(self, now):
"""Update state at a scheduled point in time."""
self.async_write_ha_state()

async def async_added_to_hass(self):
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if state:
if (
state.state == STATE_ALARM_PENDING
(
state.state == STATE_ALARM_PENDING
or state.state == STATE_ALARM_ARMING
)
and hasattr(state, "attributes")
and state.attributes["pre_pending_state"]
and state.attributes[ATTR_PREVIOUS_STATE]
):
# If in pending state, we return to the pre_pending_state
self._state = state.attributes["pre_pending_state"]
# If in arming or pending state, we return to the ATTR_PREVIOUS_STATE
self._state = state.attributes[ATTR_PREVIOUS_STATE]
self._state_ts = dt_util.utcnow()
else:
self._state = state.state
Expand Down
1 change: 1 addition & 0 deletions homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
CONF_ALIAS = "alias"
CONF_API_KEY = "api_key"
CONF_API_VERSION = "api_version"
CONF_ARMING_TIME = "arming_time"
CONF_AT = "at"
CONF_AUTH_MFA_MODULES = "auth_mfa_modules"
CONF_AUTH_PROVIDERS = "auth_providers"
Expand Down
7 changes: 7 additions & 0 deletions tests/components/alarm_control_panel/test_device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ async def test_get_triggers(hass, device_reg, entity_reg):
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
},
{
"platform": "device",
"domain": DOMAIN,
"type": "arming",
"device_id": device_entry.id,
"entity_id": f"{DOMAIN}.test_5678",
},
{
"platform": "device",
"domain": DOMAIN,
Expand Down
Loading