[alarm_control_panel] Remove redundant per-state callbacks (#12171)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -35,26 +35,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
|
||||
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
|
||||
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
|
||||
this->current_state_ = state;
|
||||
// Single state callback - triggers check get_state() for specific states
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_alarm_control_panel_update(this);
|
||||
#endif
|
||||
if (state == ACP_STATE_TRIGGERED) {
|
||||
this->triggered_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMING) {
|
||||
this->arming_callback_.call();
|
||||
} else if (state == ACP_STATE_PENDING) {
|
||||
this->pending_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMED_HOME) {
|
||||
this->armed_home_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMED_NIGHT) {
|
||||
this->armed_night_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMED_AWAY) {
|
||||
this->armed_away_callback_.call();
|
||||
} else if (state == ACP_STATE_DISARMED) {
|
||||
this->disarmed_callback_.call();
|
||||
}
|
||||
|
||||
// Cleared fires when leaving TRIGGERED state
|
||||
if (prev_state == ACP_STATE_TRIGGERED) {
|
||||
this->cleared_callback_.call();
|
||||
}
|
||||
@@ -69,34 +55,6 @@ void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback)
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
|
||||
this->triggered_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_arming_callback(std::function<void()> &&callback) {
|
||||
this->arming_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
|
||||
this->armed_home_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
|
||||
this->armed_night_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
|
||||
this->armed_away_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
|
||||
this->pending_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
|
||||
this->disarmed_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
|
||||
this->cleared_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
@@ -35,54 +35,13 @@ class AlarmControlPanel : public EntityBase {
|
||||
*/
|
||||
void publish_state(AlarmControlPanelState state);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes
|
||||
/** Add a callback for when the state of the alarm_control_panel changes.
|
||||
* Triggers can check get_state() to determine the new state.
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_triggered_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel chanes to arming
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_arming_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to pending
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_pending_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_armed_home_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_armed_night_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_armed_away_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_disarmed_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered
|
||||
*
|
||||
* @param callback The callback function
|
||||
@@ -172,23 +131,9 @@ class AlarmControlPanel : public EntityBase {
|
||||
uint32_t last_update_;
|
||||
// the call control function
|
||||
virtual void control(const AlarmControlPanelCall &call) = 0;
|
||||
// state callback
|
||||
// state callback - triggers check get_state() for specific state
|
||||
CallbackManager<void()> state_callback_{};
|
||||
// trigger callback
|
||||
CallbackManager<void()> triggered_callback_{};
|
||||
// arming callback
|
||||
CallbackManager<void()> arming_callback_{};
|
||||
// pending callback
|
||||
CallbackManager<void()> pending_callback_{};
|
||||
// armed_home callback
|
||||
CallbackManager<void()> armed_home_callback_{};
|
||||
// armed_night callback
|
||||
CallbackManager<void()> armed_night_callback_{};
|
||||
// armed_away callback
|
||||
CallbackManager<void()> armed_away_callback_{};
|
||||
// disarmed callback
|
||||
CallbackManager<void()> disarmed_callback_{};
|
||||
// clear callback
|
||||
// clear callback - fires when leaving TRIGGERED state
|
||||
CallbackManager<void()> cleared_callback_{};
|
||||
// chime callback
|
||||
CallbackManager<void()> chime_callback_{};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace esphome {
|
||||
namespace alarm_control_panel {
|
||||
|
||||
/// Trigger on any state change
|
||||
class StateTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
@@ -13,55 +14,30 @@ class StateTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
class TriggeredTrigger : public Trigger<> {
|
||||
/// Template trigger that fires when entering a specific state
|
||||
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
|
||||
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
|
||||
alarm_control_panel->add_on_state_callback([this]() {
|
||||
if (this->alarm_control_panel_->get_state() == State)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
class ArmingTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class PendingTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class ArmedHomeTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class ArmedNightTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class ArmedAwayTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class DisarmedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
// Type aliases for state-specific triggers
|
||||
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
|
||||
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
|
||||
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
|
||||
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
|
||||
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
|
||||
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
|
||||
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
|
||||
|
||||
/// Trigger when leaving TRIGGERED state (alarm cleared)
|
||||
class ClearedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
@@ -69,6 +45,7 @@ class ClearedTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
/// Trigger on chime event (zone opened while disarmed)
|
||||
class ChimeTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
@@ -76,6 +53,7 @@ class ChimeTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
/// Trigger on ready state change
|
||||
class ReadyTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
esphome:
|
||||
name: alarm-state-transitions
|
||||
friendly_name: "Alarm Control Panel State Transitions Test"
|
||||
|
||||
logger:
|
||||
|
||||
host:
|
||||
|
||||
globals:
|
||||
- id: door_sensor_state
|
||||
type: bool
|
||||
initial_value: "false"
|
||||
- id: chime_sensor_state
|
||||
type: bool
|
||||
initial_value: "false"
|
||||
|
||||
switch:
|
||||
# Switch to control the door sensor state
|
||||
- platform: template
|
||||
id: door_sensor_switch
|
||||
name: "Door Sensor Switch"
|
||||
optimistic: true
|
||||
turn_on_action:
|
||||
- globals.set:
|
||||
id: door_sensor_state
|
||||
value: "true"
|
||||
turn_off_action:
|
||||
- globals.set:
|
||||
id: door_sensor_state
|
||||
value: "false"
|
||||
# Switch to control the chime sensor state
|
||||
- platform: template
|
||||
id: chime_sensor_switch
|
||||
name: "Chime Sensor Switch"
|
||||
optimistic: true
|
||||
turn_on_action:
|
||||
- globals.set:
|
||||
id: chime_sensor_state
|
||||
value: "true"
|
||||
turn_off_action:
|
||||
- globals.set:
|
||||
id: chime_sensor_state
|
||||
value: "false"
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: door_sensor
|
||||
name: "Door Sensor"
|
||||
lambda: |-
|
||||
return id(door_sensor_state);
|
||||
- platform: template
|
||||
id: chime_sensor
|
||||
name: "Chime Sensor"
|
||||
lambda: |-
|
||||
return id(chime_sensor_state);
|
||||
|
||||
alarm_control_panel:
|
||||
- platform: template
|
||||
id: test_alarm
|
||||
name: "Test Alarm"
|
||||
codes:
|
||||
- "1234"
|
||||
requires_code_to_arm: true
|
||||
# Short timeouts for faster testing
|
||||
arming_away_time: 50ms
|
||||
arming_home_time: 50ms
|
||||
arming_night_time: 50ms
|
||||
pending_time: 50ms
|
||||
trigger_time: 100ms
|
||||
restore_mode: ALWAYS_DISARMED
|
||||
binary_sensors:
|
||||
- input: door_sensor
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
chime: false
|
||||
trigger_mode: DELAYED
|
||||
- input: chime_sensor
|
||||
bypass_armed_home: true
|
||||
bypass_armed_night: true
|
||||
chime: true
|
||||
trigger_mode: DELAYED
|
||||
on_state:
|
||||
- logger.log: "State changed"
|
||||
on_disarmed:
|
||||
- logger.log: "Alarm disarmed"
|
||||
on_arming:
|
||||
- logger.log: "Alarm arming"
|
||||
on_armed_away:
|
||||
- logger.log: "Alarm armed away"
|
||||
on_armed_home:
|
||||
- logger.log: "Alarm armed home"
|
||||
on_armed_night:
|
||||
- logger.log: "Alarm armed night"
|
||||
on_pending:
|
||||
- logger.log: "Alarm pending"
|
||||
on_triggered:
|
||||
- logger.log: "Alarm triggered"
|
||||
on_cleared:
|
||||
- logger.log: "Alarm cleared"
|
||||
on_chime:
|
||||
- logger.log: "Chime activated"
|
||||
on_ready:
|
||||
- logger.log: "Sensors ready state changed"
|
||||
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
319
tests/integration/test_alarm_control_panel_state_transitions.py
Normal file
319
tests/integration/test_alarm_control_panel_state_transitions.py
Normal file
@@ -0,0 +1,319 @@
|
||||
"""Integration test for alarm control panel state transitions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import (
|
||||
AlarmControlPanelCommand,
|
||||
AlarmControlPanelEntityState,
|
||||
AlarmControlPanelInfo,
|
||||
AlarmControlPanelState,
|
||||
SwitchInfo,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_alarm_control_panel_state_transitions(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test alarm control panel state transitions.
|
||||
|
||||
This comprehensive test verifies all state transitions and listener callbacks:
|
||||
|
||||
1. Basic arm/disarm sequences:
|
||||
- DISARMED -> ARMING -> ARMED_AWAY -> DISARMED
|
||||
- DISARMED -> ARMING -> ARMED_HOME -> DISARMED
|
||||
- DISARMED -> ARMING -> ARMED_NIGHT -> DISARMED
|
||||
|
||||
2. Wrong code rejection
|
||||
|
||||
3. Sensor triggering while armed:
|
||||
- ARMED_AWAY -> PENDING -> TRIGGERED (delayed sensor)
|
||||
- TRIGGERED -> ARMED_AWAY (auto-reset after trigger_time, fires on_cleared)
|
||||
|
||||
4. Chime functionality:
|
||||
- Sensor open while DISARMED triggers on_chime
|
||||
|
||||
5. Ready state:
|
||||
- Sensor state changes trigger on_ready
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track log messages for callback verification
|
||||
log_lines: list[str] = []
|
||||
chime_future: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures: list[asyncio.Future[bool]] = []
|
||||
cleared_future: asyncio.Future[bool] = loop.create_future()
|
||||
|
||||
# Patterns to match log output from callbacks
|
||||
chime_pattern = re.compile(r"Chime activated")
|
||||
ready_pattern = re.compile(r"Sensors ready state changed")
|
||||
cleared_pattern = re.compile(r"Alarm cleared")
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
log_lines.append(line)
|
||||
if not chime_future.done() and chime_pattern.search(line):
|
||||
chime_future.set_result(True)
|
||||
if ready_pattern.search(line):
|
||||
# Create new future for each ready event
|
||||
for fut in ready_futures:
|
||||
if not fut.done():
|
||||
fut.set_result(True)
|
||||
break
|
||||
if not cleared_future.done() and cleared_pattern.search(line):
|
||||
cleared_future.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find entities
|
||||
alarm_info: AlarmControlPanelInfo | None = None
|
||||
door_switch_info: SwitchInfo | None = None
|
||||
chime_switch_info: SwitchInfo | None = None
|
||||
|
||||
for entity in entities:
|
||||
if isinstance(entity, AlarmControlPanelInfo):
|
||||
alarm_info = entity
|
||||
elif isinstance(entity, SwitchInfo):
|
||||
if entity.name == "Door Sensor Switch":
|
||||
door_switch_info = entity
|
||||
elif entity.name == "Chime Sensor Switch":
|
||||
chime_switch_info = entity
|
||||
|
||||
assert alarm_info is not None, "Alarm control panel not found"
|
||||
assert door_switch_info is not None, "Door sensor switch not found"
|
||||
assert chime_switch_info is not None, "Chime sensor switch not found"
|
||||
|
||||
# Track state changes
|
||||
states_received: list[AlarmControlPanelState] = []
|
||||
state_event = asyncio.Event()
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
if (
|
||||
isinstance(state, AlarmControlPanelEntityState)
|
||||
and state.key == alarm_info.key
|
||||
):
|
||||
states_received.append(state.state)
|
||||
state_event.set()
|
||||
|
||||
# Use InitialStateHelper to handle initial state broadcast
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for initial states from all entities
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# Verify alarm panel started in DISARMED state
|
||||
initial_alarm_state = initial_state_helper.initial_states.get(alarm_info.key)
|
||||
assert initial_alarm_state is not None, "No initial alarm state received"
|
||||
assert isinstance(initial_alarm_state, AlarmControlPanelEntityState)
|
||||
assert initial_alarm_state.state == AlarmControlPanelState.DISARMED
|
||||
|
||||
# Helper to wait for specific state
|
||||
async def wait_for_state(
|
||||
expected: AlarmControlPanelState, timeout: float = 5.0
|
||||
) -> None:
|
||||
deadline = loop.time() + timeout
|
||||
while True:
|
||||
remaining = deadline - loop.time()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError(
|
||||
f"Timeout waiting for state {expected}, "
|
||||
f"last state: {states_received[-1] if states_received else 'none'}"
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=remaining)
|
||||
state_event.clear()
|
||||
if states_received[-1] == expected:
|
||||
return
|
||||
|
||||
# ===== Test wrong code rejection =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_AWAY,
|
||||
code="0000", # Wrong code
|
||||
)
|
||||
|
||||
# Should NOT transition - wait a bit and verify no state changes
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await asyncio.wait_for(state_event.wait(), timeout=0.5)
|
||||
# No state changes should have occurred (list is empty)
|
||||
assert len(states_received) == 0, f"Unexpected state changes: {states_received}"
|
||||
|
||||
# ===== Test ARM_AWAY sequence =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_AWAY,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.ARMING)
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_AWAY)
|
||||
|
||||
# Disarm
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.DISARM,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# ===== Test ARM_HOME sequence =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_HOME,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.ARMING)
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_HOME)
|
||||
|
||||
# Disarm
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.DISARM,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# ===== Test ARM_NIGHT sequence =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_NIGHT,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.ARMING)
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_NIGHT)
|
||||
|
||||
# Disarm
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.DISARM,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# Verify basic state sequence (initial DISARMED is handled by InitialStateHelper)
|
||||
expected_states = [
|
||||
AlarmControlPanelState.ARMING, # Arm away
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
AlarmControlPanelState.ARMING, # Arm home
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
AlarmControlPanelState.ARMING, # Arm night
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
]
|
||||
assert states_received == expected_states, (
|
||||
f"State sequence mismatch.\nExpected: {expected_states}\n"
|
||||
f"Got: {states_received}"
|
||||
)
|
||||
|
||||
# ===== Test PENDING -> TRIGGERED -> CLEARED sequence =====
|
||||
# This tests on_pending, on_triggered, and on_cleared callbacks
|
||||
|
||||
# Arm away first
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.ARM_AWAY,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.ARMING)
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_AWAY)
|
||||
|
||||
# Trip the door sensor (delayed mode triggers PENDING first)
|
||||
client.switch_command(door_switch_info.key, True)
|
||||
|
||||
# Should go to PENDING (delayed sensor)
|
||||
await wait_for_state(AlarmControlPanelState.PENDING)
|
||||
|
||||
# Should go to TRIGGERED after pending_time (50ms)
|
||||
await wait_for_state(AlarmControlPanelState.TRIGGERED)
|
||||
|
||||
# Close the sensor
|
||||
client.switch_command(door_switch_info.key, False)
|
||||
|
||||
# Wait for trigger_time to expire and auto-reset (100ms)
|
||||
# The alarm should go back to ARMED_AWAY after trigger_time
|
||||
# This transition FROM TRIGGERED fires on_cleared
|
||||
await wait_for_state(AlarmControlPanelState.ARMED_AWAY, timeout=2.0)
|
||||
|
||||
# Verify on_cleared was logged
|
||||
try:
|
||||
await asyncio.wait_for(cleared_future, timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"on_cleared callback not fired. Log lines: {log_lines[-20:]}")
|
||||
|
||||
# Disarm
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
AlarmControlPanelCommand.DISARM,
|
||||
code="1234",
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# Verify trigger sequence was added
|
||||
assert AlarmControlPanelState.PENDING in states_received
|
||||
assert AlarmControlPanelState.TRIGGERED in states_received
|
||||
|
||||
# ===== Test chime (sensor open while disarmed) =====
|
||||
# The chime_sensor has chime: true, so opening it while disarmed
|
||||
# should trigger on_chime callback
|
||||
|
||||
# We're currently DISARMED - open the chime sensor
|
||||
client.switch_command(chime_switch_info.key, True)
|
||||
|
||||
# Wait for chime callback to be logged
|
||||
try:
|
||||
await asyncio.wait_for(chime_future, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}")
|
||||
|
||||
# Close the chime sensor
|
||||
client.switch_command(chime_switch_info.key, False)
|
||||
|
||||
# ===== Test ready state changes =====
|
||||
# Opening/closing sensors while disarmed affects ready state
|
||||
# The on_ready callback fires when sensors_ready changes
|
||||
|
||||
# Set up futures for ready state changes
|
||||
ready_future_1: asyncio.Future[bool] = loop.create_future()
|
||||
ready_future_2: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures.extend([ready_future_1, ready_future_2])
|
||||
|
||||
# Open door sensor (makes alarm not ready)
|
||||
client.switch_command(door_switch_info.key, True)
|
||||
|
||||
# Wait for first on_ready callback (not ready)
|
||||
try:
|
||||
await asyncio.wait_for(ready_future_1, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"on_ready callback not fired when sensor opened. "
|
||||
f"Log lines: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
# Close door sensor (makes alarm ready again)
|
||||
client.switch_command(door_switch_info.key, False)
|
||||
|
||||
# Wait for second on_ready callback (ready)
|
||||
try:
|
||||
await asyncio.wait_for(ready_future_2, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"on_ready callback not fired when sensor closed. "
|
||||
f"Log lines: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
# Final state should still be DISARMED
|
||||
assert states_received[-1] == AlarmControlPanelState.DISARMED
|
||||
Reference in New Issue
Block a user