mirror of
https://github.com/esphome/esphome.git
synced 2026-02-18 15:35:59 -07:00
reduce
This commit is contained in:
@@ -35,15 +35,15 @@ 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;
|
||||
|
||||
for (auto *listener : this->state_listeners_) {
|
||||
listener->on_state(state, prev_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
|
||||
|
||||
// Cleared fires when leaving TRIGGERED state
|
||||
if (prev_state == ACP_STATE_TRIGGERED) {
|
||||
this->cleared_callback_.call();
|
||||
}
|
||||
if (state == this->desired_state_) {
|
||||
// only store when in the desired state
|
||||
this->pref_.save(&state);
|
||||
@@ -51,14 +51,20 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
|
||||
}
|
||||
}
|
||||
|
||||
void AlarmControlPanel::notify_chime() {
|
||||
for (auto *listener : this->event_listeners_)
|
||||
listener->on_chime();
|
||||
void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::notify_ready() {
|
||||
for (auto *listener : this->event_listeners_)
|
||||
listener->on_ready();
|
||||
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
|
||||
this->cleared_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
|
||||
this->chime_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
|
||||
this->ready_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_away(optional<std::string> code) {
|
||||
|
||||
@@ -1,37 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
#include "alarm_control_panel_call.h"
|
||||
#include "alarm_control_panel_state.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace alarm_control_panel {
|
||||
|
||||
/// Listener interface for alarm control panel state changes.
|
||||
class AlarmControlPanelStateListener {
|
||||
public:
|
||||
virtual ~AlarmControlPanelStateListener() = default;
|
||||
/// Called when state changes. Check new_state/prev_state to filter specific states.
|
||||
virtual void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) = 0;
|
||||
};
|
||||
|
||||
/// Listener interface for alarm events (chime, ready, etc).
|
||||
class AlarmControlPanelEventListener {
|
||||
public:
|
||||
virtual ~AlarmControlPanelEventListener() = default;
|
||||
/// Called when a chime zone opens while disarmed.
|
||||
virtual void on_chime() {}
|
||||
/// Called when zones ready state changes.
|
||||
virtual void on_ready() {}
|
||||
};
|
||||
|
||||
enum AlarmControlPanelFeature : uint8_t {
|
||||
// Matches Home Assistant values
|
||||
ACP_FEAT_ARM_HOME = 1 << 0,
|
||||
@@ -55,25 +35,30 @@ class AlarmControlPanel : public EntityBase {
|
||||
*/
|
||||
void publish_state(AlarmControlPanelState state);
|
||||
|
||||
/** Register a listener for state 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 listener The listener to add (must remain valid for lifetime of panel)
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_listener(AlarmControlPanelStateListener *listener) { this->state_listeners_.push_back(listener); }
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Register a listener for alarm events (chime/ready/etc).
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered
|
||||
*
|
||||
* @param listener The listener to add (must remain valid for lifetime of panel)
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_listener(AlarmControlPanelEventListener *listener) { this->event_listeners_.push_back(listener); }
|
||||
void add_on_cleared_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Notify listeners of a chime event (zone opened while disarmed).
|
||||
/** Add a callback for when a chime zone goes from closed to open
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void notify_chime();
|
||||
void add_on_chime_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Notify listeners of a ready state change.
|
||||
/** Add a callback for when a ready state changes
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void notify_ready();
|
||||
void add_on_ready_callback(std::function<void()> &&callback);
|
||||
|
||||
/** A numeric representation of the supported features as per HomeAssistant
|
||||
*
|
||||
@@ -146,10 +131,14 @@ class AlarmControlPanel : public EntityBase {
|
||||
uint32_t last_update_;
|
||||
// the call control function
|
||||
virtual void control(const AlarmControlPanelCall &call) = 0;
|
||||
// registered state listeners
|
||||
std::vector<AlarmControlPanelStateListener *> state_listeners_;
|
||||
// registered event listeners (chime/ready/etc)
|
||||
std::vector<AlarmControlPanelEventListener *> event_listeners_;
|
||||
// state callback - triggers check get_state() for specific state
|
||||
CallbackManager<void()> state_callback_{};
|
||||
// clear callback - fires when leaving TRIGGERED state
|
||||
CallbackManager<void()> cleared_callback_{};
|
||||
// chime callback
|
||||
CallbackManager<void()> chime_callback_{};
|
||||
// ready callback
|
||||
CallbackManager<void()> ready_callback_{};
|
||||
};
|
||||
|
||||
} // namespace alarm_control_panel
|
||||
|
||||
@@ -7,54 +7,133 @@ namespace esphome {
|
||||
namespace alarm_control_panel {
|
||||
|
||||
/// Trigger on any state change
|
||||
class StateTrigger final : public Trigger<>, public AlarmControlPanelStateListener {
|
||||
class StateTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); }
|
||||
void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { this->trigger(); }
|
||||
};
|
||||
|
||||
/// Template trigger that fires when entering a specific state
|
||||
template<AlarmControlPanelState State>
|
||||
class StateEnterTrigger final : public Trigger<>, public AlarmControlPanelStateListener {
|
||||
public:
|
||||
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); }
|
||||
void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override {
|
||||
if (new_state == State)
|
||||
this->trigger();
|
||||
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_state_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 entering TRIGGERED state
|
||||
class TriggeredTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit TriggeredTrigger(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() == ACP_STATE_TRIGGERED)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
/// Trigger when entering ARMING state
|
||||
class ArmingTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmingTrigger(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() == ACP_STATE_ARMING)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
/// Trigger when entering PENDING state
|
||||
class PendingTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit PendingTrigger(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() == ACP_STATE_PENDING)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
/// Trigger when entering ARMED_HOME state
|
||||
class ArmedHomeTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedHomeTrigger(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() == ACP_STATE_ARMED_HOME)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
/// Trigger when entering ARMED_NIGHT state
|
||||
class ArmedNightTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedNightTrigger(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() == ACP_STATE_ARMED_NIGHT)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
/// Trigger when entering ARMED_AWAY state
|
||||
class ArmedAwayTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ArmedAwayTrigger(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() == ACP_STATE_ARMED_AWAY)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
/// Trigger when entering DISARMED state
|
||||
class DisarmedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit DisarmedTrigger(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() == ACP_STATE_DISARMED)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
/// Trigger when leaving TRIGGERED state (alarm cleared)
|
||||
class ClearedTrigger final : public Trigger<>, public AlarmControlPanelStateListener {
|
||||
class ClearedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); }
|
||||
void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override {
|
||||
if (prev_state == ACP_STATE_TRIGGERED)
|
||||
this->trigger();
|
||||
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
/// Trigger on chime event (zone opened while disarmed)
|
||||
class ChimeTrigger final : public Trigger<>, public AlarmControlPanelEventListener {
|
||||
class ChimeTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); }
|
||||
void on_chime() override { this->trigger(); }
|
||||
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
/// Trigger on ready state change
|
||||
class ReadyTrigger final : public Trigger<>, public AlarmControlPanelEventListener {
|
||||
class ReadyTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); }
|
||||
void on_ready() override { this->trigger(); }
|
||||
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace esphome::alarm_control_panel;
|
||||
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
|
||||
: alarm_control_panel_(alarm_control_panel) {}
|
||||
void MQTTAlarmControlPanelComponent::setup() {
|
||||
this->alarm_control_panel_->add_listener(this);
|
||||
this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); });
|
||||
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||
auto call = this->alarm_control_panel_->make_call();
|
||||
if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) {
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
|
||||
class MQTTAlarmControlPanelComponent final : public mqtt::MQTTComponent,
|
||||
public alarm_control_panel::AlarmControlPanelStateListener {
|
||||
class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent {
|
||||
public:
|
||||
explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel);
|
||||
|
||||
@@ -26,12 +25,6 @@ class MQTTAlarmControlPanelComponent final : public mqtt::MQTTComponent,
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
// AlarmControlPanelStateListener interface
|
||||
void on_state(alarm_control_panel::AlarmControlPanelState new_state,
|
||||
alarm_control_panel::AlarmControlPanelState prev_state) override {
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string component_type() const override;
|
||||
const EntityBase *get_entity() const override;
|
||||
|
||||
@@ -15,6 +15,7 @@ from aioesphomeapi import (
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@@ -107,7 +108,18 @@ async def test_alarm_control_panel_state_transitions(
|
||||
states_received.append(state.state)
|
||||
state_event.set()
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
# 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(
|
||||
@@ -126,9 +138,6 @@ async def test_alarm_control_panel_state_transitions(
|
||||
if states_received[-1] == expected:
|
||||
return
|
||||
|
||||
# Wait for initial DISARMED state
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# ===== Test wrong code rejection =====
|
||||
client.alarm_control_panel_command(
|
||||
alarm_info.key,
|
||||
@@ -136,10 +145,11 @@ async def test_alarm_control_panel_state_transitions(
|
||||
code="0000", # Wrong code
|
||||
)
|
||||
|
||||
# Should NOT transition - wait a bit and verify still disarmed
|
||||
# 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)
|
||||
assert states_received[-1] == AlarmControlPanelState.DISARMED
|
||||
# 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(
|
||||
@@ -192,9 +202,8 @@ async def test_alarm_control_panel_state_transitions(
|
||||
)
|
||||
await wait_for_state(AlarmControlPanelState.DISARMED)
|
||||
|
||||
# Verify basic state sequence
|
||||
# Verify basic state sequence (initial DISARMED is handled by InitialStateHelper)
|
||||
expected_states = [
|
||||
AlarmControlPanelState.DISARMED, # Initial
|
||||
AlarmControlPanelState.ARMING, # Arm away
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
|
||||
Reference in New Issue
Block a user