[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)),
|
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)));
|
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
|
||||||
this->current_state_ = state;
|
this->current_state_ = state;
|
||||||
|
// Single state callback - triggers check get_state() for specific states
|
||||||
this->state_callback_.call();
|
this->state_callback_.call();
|
||||||
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
|
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
|
||||||
ControllerRegistry::notify_alarm_control_panel_update(this);
|
ControllerRegistry::notify_alarm_control_panel_update(this);
|
||||||
#endif
|
#endif
|
||||||
if (state == ACP_STATE_TRIGGERED) {
|
// Cleared fires when leaving TRIGGERED state
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prev_state == ACP_STATE_TRIGGERED) {
|
if (prev_state == ACP_STATE_TRIGGERED) {
|
||||||
this->cleared_callback_.call();
|
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));
|
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) {
|
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
|
||||||
this->cleared_callback_.add(std::move(callback));
|
this->cleared_callback_.add(std::move(callback));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,54 +35,13 @@ class AlarmControlPanel : public EntityBase {
|
|||||||
*/
|
*/
|
||||||
void publish_state(AlarmControlPanelState state);
|
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
|
* @param callback The callback function
|
||||||
*/
|
*/
|
||||||
void add_on_state_callback(std::function<void()> &&callback);
|
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
|
/** Add a callback for when the state of the alarm_control_panel clears from triggered
|
||||||
*
|
*
|
||||||
* @param callback The callback function
|
* @param callback The callback function
|
||||||
@@ -172,23 +131,9 @@ class AlarmControlPanel : public EntityBase {
|
|||||||
uint32_t last_update_;
|
uint32_t last_update_;
|
||||||
// the call control function
|
// the call control function
|
||||||
virtual void control(const AlarmControlPanelCall &call) = 0;
|
virtual void control(const AlarmControlPanelCall &call) = 0;
|
||||||
// state callback
|
// state callback - triggers check get_state() for specific state
|
||||||
CallbackManager<void()> state_callback_{};
|
CallbackManager<void()> state_callback_{};
|
||||||
// trigger callback
|
// clear callback - fires when leaving TRIGGERED state
|
||||||
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
|
|
||||||
CallbackManager<void()> cleared_callback_{};
|
CallbackManager<void()> cleared_callback_{};
|
||||||
// chime callback
|
// chime callback
|
||||||
CallbackManager<void()> chime_callback_{};
|
CallbackManager<void()> chime_callback_{};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace alarm_control_panel {
|
namespace alarm_control_panel {
|
||||||
|
|
||||||
|
/// Trigger on any state change
|
||||||
class StateTrigger : public Trigger<> {
|
class StateTrigger : public Trigger<> {
|
||||||
public:
|
public:
|
||||||
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
|
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:
|
public:
|
||||||
explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
|
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
|
||||||
alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
|
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<> {
|
// Type aliases for state-specific triggers
|
||||||
public:
|
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
|
||||||
explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
|
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
|
||||||
alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
|
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>;
|
||||||
class PendingTrigger : public Trigger<> {
|
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
|
||||||
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(); });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/// Trigger when leaving TRIGGERED state (alarm cleared)
|
||||||
class ClearedTrigger : public Trigger<> {
|
class ClearedTrigger : public Trigger<> {
|
||||||
public:
|
public:
|
||||||
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
|
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<> {
|
class ChimeTrigger : public Trigger<> {
|
||||||
public:
|
public:
|
||||||
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
|
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||||
@@ -76,6 +53,7 @@ class ChimeTrigger : public Trigger<> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Trigger on ready state change
|
||||||
class ReadyTrigger : public Trigger<> {
|
class ReadyTrigger : public Trigger<> {
|
||||||
public:
|
public:
|
||||||
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
|
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