diff --git a/CODEOWNERS b/CODEOWNERS index 1b75203654..c6cbf3c2ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,6 +19,7 @@ esphome/components/addressable_light/* @justfalter esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban +esphome/components/alarm_control_panel/* @grahambrown11 esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/am43/sensor/* @buxtronix @@ -277,6 +278,7 @@ esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax +esphome/components/template/alarm_control_panel/* @grahambrown11 esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter esphome/components/tlc5947/* @rnauber diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py new file mode 100644 index 0000000000..963d5ae719 --- /dev/null +++ b/esphome/components/alarm_control_panel/__init__.py @@ -0,0 +1,165 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.core import CORE, coroutine_with_priority +from esphome.const import ( + CONF_ID, + CONF_ON_STATE, + CONF_TRIGGER_ID, + CONF_CODE, +) +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@grahambrown11"] +IS_PLATFORM_COMPONENT = True + +CONF_ON_TRIGGERED = "on_triggered" +CONF_ON_CLEARED = "on_cleared" + +alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") +AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) + +StateTrigger = alarm_control_panel_ns.class_( + "StateTrigger", automation.Trigger.template() +) +TriggeredTrigger = alarm_control_panel_ns.class_( + "TriggeredTrigger", automation.Trigger.template() +) +ClearedTrigger = alarm_control_panel_ns.class_( + "ClearedTrigger", automation.Trigger.template() +) +ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) +ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) +DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action) +PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action) +TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action) +AlarmControlPanelCondition = alarm_control_panel_ns.class_( + "AlarmControlPanelCondition", automation.Condition +) + +ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AlarmControlPanel), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + } + ), + cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), + } + ), + cv.Optional(CONF_ON_CLEARED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), + } + ), + } +) + +ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(AlarmControlPanel), + cv.Optional(CONF_CODE): cv.templatable(cv.string), + } +) + +ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(AlarmControlPanel), + } +) + + +async def setup_alarm_control_panel_core_(var, config): + await setup_entity(var, config) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TRIGGERED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CLEARED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +async def register_alarm_control_panel(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_alarm_control_panel(var)) + await setup_alarm_control_panel_core_(var, config) + + +@automation.register_action( + "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_CODE in config: + templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_CODE in config: + templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_disarm_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_CODE in config: + templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_pending_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_trigger_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_condition( + "alarm_control_panel.is_armed", + AlarmControlPanelCondition, + ALARM_CONTROL_PANEL_CONDITION_SCHEMA, +) +async def alarm_control_panel_is_armed_to_code( + config, condition_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(alarm_control_panel_ns.using) + cg.add_define("USE_ALARM_CONTROL_PANEL") diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp new file mode 100644 index 0000000000..74c9a502df --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -0,0 +1,111 @@ +#include + +#include "alarm_control_panel.h" + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +static const char *const TAG = "alarm_control_panel"; + +AlarmControlPanelCall AlarmControlPanel::make_call() { return AlarmControlPanelCall(this); } + +bool AlarmControlPanel::is_state_armed(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_ARMED_AWAY: + case ACP_STATE_ARMED_HOME: + case ACP_STATE_ARMED_NIGHT: + case ACP_STATE_ARMED_VACATION: + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return true; + default: + return false; + } +}; + +void AlarmControlPanel::publish_state(AlarmControlPanelState state) { + this->last_update_ = millis(); + if (state != this->current_state_) { + auto prev_state = this->current_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; + this->state_callback_.call(); + if (state == ACP_STATE_TRIGGERED) { + this->triggered_callback_.call(); + } + 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); + } + } +} + +void AlarmControlPanel::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::add_on_triggered_callback(std::function &&callback) { + this->triggered_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::add_on_cleared_callback(std::function &&callback) { + this->cleared_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::arm_away(optional code) { + auto call = this->make_call(); + call.arm_away(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_home(optional code) { + auto call = this->make_call(); + call.arm_home(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_night(optional code) { + auto call = this->make_call(); + call.arm_night(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_vacation(optional code) { + auto call = this->make_call(); + call.arm_vacation(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_custom_bypass(optional code) { + auto call = this->make_call(); + call.arm_custom_bypass(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::disarm(optional code) { + auto call = this->make_call(); + call.disarm(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h new file mode 100644 index 0000000000..4f15ccb45a --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -0,0 +1,136 @@ +#pragma once + +#include + +#include "alarm_control_panel_call.h" +#include "alarm_control_panel_state.h" + +#include "esphome/core/automation.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +enum AlarmControlPanelFeature : uint8_t { + // Matches Home Assistant values + ACP_FEAT_ARM_HOME = 1 << 0, + ACP_FEAT_ARM_AWAY = 1 << 1, + ACP_FEAT_ARM_NIGHT = 1 << 2, + ACP_FEAT_TRIGGER = 1 << 3, + ACP_FEAT_ARM_CUSTOM_BYPASS = 1 << 4, + ACP_FEAT_ARM_VACATION = 1 << 5, +}; + +class AlarmControlPanel : public EntityBase { + public: + /** Make a AlarmControlPanelCall + * + */ + AlarmControlPanelCall make_call(); + + /** Set the state of the alarm_control_panel. + * + * @param state The AlarmControlPanelState. + */ + void publish_state(AlarmControlPanelState state); + + /** Add a callback for when the state of the alarm_control_panel changes + * + * @param callback The callback function + */ + void add_on_state_callback(std::function &&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 &&callback); + + /** Add a callback for when the state of the alarm_control_panel clears from triggered + * + * @param callback The callback function + */ + void add_on_cleared_callback(std::function &&callback); + + /** A numeric representation of the supported features as per HomeAssistant + * + */ + virtual uint32_t get_supported_features() const = 0; + + /** Returns if the alarm_control_panel has a code + * + */ + virtual bool get_requires_code() const = 0; + + /** Returns if the alarm_control_panel requires a code to arm + * + */ + virtual bool get_requires_code_to_arm() const = 0; + + /** arm the alarm in away mode + * + * @param code The code + */ + void arm_away(optional code = nullopt); + + /** arm the alarm in home mode + * + * @param code The code + */ + void arm_home(optional code = nullopt); + + /** arm the alarm in night mode + * + * @param code The code + */ + void arm_night(optional code = nullopt); + + /** arm the alarm in vacation mode + * + * @param code The code + */ + void arm_vacation(optional code = nullopt); + + /** arm the alarm in custom bypass mode + * + * @param code The code + */ + void arm_custom_bypass(optional code = nullopt); + + /** disarm the alarm + * + * @param code The code + */ + void disarm(optional code = nullopt); + + /** Get the state + * + */ + AlarmControlPanelState get_state() const { return this->current_state_; } + + // is the state one of the armed states + bool is_state_armed(AlarmControlPanelState state); + + protected: + friend AlarmControlPanelCall; + // in order to store last panel state in flash + ESPPreferenceObject pref_; + // current state + AlarmControlPanelState current_state_; + // the desired (or previous) state + AlarmControlPanelState desired_state_; + // last time the state was updated + uint32_t last_update_; + // the call control function + virtual void control(const AlarmControlPanelCall &call) = 0; + // state callback + CallbackManager state_callback_{}; + // trigger callback + CallbackManager triggered_callback_{}; + // clear callback + CallbackManager cleared_callback_{}; +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp new file mode 100644 index 0000000000..eb50c4f4b5 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -0,0 +1,99 @@ +#include "alarm_control_panel_call.h" + +#include "alarm_control_panel.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +static const char *const TAG = "alarm_control_panel"; + +AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {} + +AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) { + this->code_ = code; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_away() { + this->state_ = ACP_STATE_ARMED_AWAY; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_home() { + this->state_ = ACP_STATE_ARMED_HOME; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_night() { + this->state_ = ACP_STATE_ARMED_NIGHT; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_vacation() { + this->state_ = ACP_STATE_ARMED_VACATION; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_custom_bypass() { + this->state_ = ACP_STATE_ARMED_CUSTOM_BYPASS; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::disarm() { + this->state_ = ACP_STATE_DISARMED; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::pending() { + this->state_ = ACP_STATE_PENDING; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::triggered() { + this->state_ = ACP_STATE_TRIGGERED; + return *this; +} + +const optional &AlarmControlPanelCall::get_state() const { return this->state_; } +const optional &AlarmControlPanelCall::get_code() const { return this->code_; } + +void AlarmControlPanelCall::validate_() { + if (this->state_.has_value()) { + auto state = *this->state_; + if (this->parent_->is_state_armed(state) && this->parent_->get_state() != ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot arm when not disarmed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_PENDING && this->parent_->get_state() == ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot trip alarm when disarmed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_DISARMED && + !(this->parent_->is_state_armed(this->parent_->get_state()) || + this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_ARMING || + this->parent_->get_state() == ACP_STATE_TRIGGERED)) { + ESP_LOGW(TAG, "Cannot disarm when not armed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_ARMED_HOME && (this->parent_->get_supported_features() & ACP_FEAT_ARM_HOME) == 0) { + ESP_LOGW(TAG, "Cannot arm home when not supported"); + this->state_.reset(); + return; + } + } +} + +void AlarmControlPanelCall::perform() { + this->validate_(); + if (this->state_) { + this->parent_->control(*this); + } +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h new file mode 100644 index 0000000000..034e3142da --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "alarm_control_panel_state.h" + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace alarm_control_panel { + +class AlarmControlPanel; + +class AlarmControlPanelCall { + public: + AlarmControlPanelCall(AlarmControlPanel *parent); + + AlarmControlPanelCall &set_code(const std::string &code); + AlarmControlPanelCall &arm_away(); + AlarmControlPanelCall &arm_home(); + AlarmControlPanelCall &arm_night(); + AlarmControlPanelCall &arm_vacation(); + AlarmControlPanelCall &arm_custom_bypass(); + AlarmControlPanelCall &disarm(); + AlarmControlPanelCall &pending(); + AlarmControlPanelCall &triggered(); + + void perform(); + const optional &get_state() const; + const optional &get_code() const; + + protected: + AlarmControlPanel *parent_; + optional code_{}; + optional state_{}; + void validate_(); +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp new file mode 100644 index 0000000000..231e7228e1 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -0,0 +1,34 @@ +#include "alarm_control_panel_state.h" + +namespace esphome { +namespace alarm_control_panel { + +const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_DISARMED: + return LOG_STR("DISARMED"); + case ACP_STATE_ARMED_HOME: + return LOG_STR("ARMED_HOME"); + case ACP_STATE_ARMED_AWAY: + return LOG_STR("ARMED_AWAY"); + case ACP_STATE_ARMED_NIGHT: + return LOG_STR("NIGHT"); + case ACP_STATE_ARMED_VACATION: + return LOG_STR("ARMED_VACATION"); + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return LOG_STR("ARMED_CUSTOM_BYPASS"); + case ACP_STATE_PENDING: + return LOG_STR("PENDING"); + case ACP_STATE_ARMING: + return LOG_STR("ARMING"); + case ACP_STATE_DISARMING: + return LOG_STR("DISARMING"); + case ACP_STATE_TRIGGERED: + return LOG_STR("TRIGGERED"); + default: + return LOG_STR("UNKNOWN"); + } +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.h b/esphome/components/alarm_control_panel/alarm_control_panel_state.h new file mode 100644 index 0000000000..ad16222dc0 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +enum AlarmControlPanelState : uint8_t { + ACP_STATE_DISARMED = 0, + ACP_STATE_ARMED_HOME = 1, + ACP_STATE_ARMED_AWAY = 2, + ACP_STATE_ARMED_NIGHT = 3, + ACP_STATE_ARMED_VACATION = 4, + ACP_STATE_ARMED_CUSTOM_BYPASS = 5, + ACP_STATE_PENDING = 6, + ACP_STATE_ARMING = 7, + ACP_STATE_DISARMING = 8, + ACP_STATE_TRIGGERED = 9 +}; + +/** Returns a string representation of the state. + * + * @param state The AlarmControlPanelState. + */ +const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state); + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h new file mode 100644 index 0000000000..4368129609 --- /dev/null +++ b/esphome/components/alarm_control_panel/automation.h @@ -0,0 +1,115 @@ + +#pragma once +#include "esphome/core/automation.h" +#include "alarm_control_panel.h" + +namespace esphome { +namespace alarm_control_panel { + +class StateTrigger : public Trigger<> { + public: + explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); + } +}; + +class TriggeredTrigger : public Trigger<> { + public: + explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); }); + } +}; + +class ClearedTrigger : public Trigger<> { + public: + explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); }); + } +}; + +template class ArmAwayAction : public Action { + public: + explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { + auto call = this->alarm_control_panel_->make_call(); + auto code = this->code_.optional_value(x...); + if (code.has_value()) { + call.set_code(code.value()); + } + call.arm_away(); + call.perform(); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class ArmHomeAction : public Action { + public: + explicit ArmHomeAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { + auto call = this->alarm_control_panel_->make_call(); + auto code = this->code_.optional_value(x...); + if (code.has_value()) { + call.set_code(code.value()); + } + call.arm_home(); + call.perform(); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class DisarmAction : public Action { + public: + explicit DisarmAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class PendingAction : public Action { + public: + explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class TriggeredAction : public Action { + public: + explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class AlarmControlPanelCondition : public Condition { + public: + AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} + bool check(Ts... x) override { + return this->parent_->is_state_armed(this->parent_->get_state()) || + this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; + } + + protected: + AlarmControlPanel *parent_; +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 74d08195a0..0d68d9fe55 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -56,6 +56,8 @@ service APIConnection { rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} + + rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {} } @@ -1454,3 +1456,63 @@ message VoiceAssistantEventResponse { VoiceAssistantEvent event_type = 1; repeated VoiceAssistantEventData data = 2; } + +// ==================== ALARM CONTROL PANEL ==================== +enum AlarmControlPanelState { + ALARM_STATE_DISARMED = 0; + ALARM_STATE_ARMED_HOME = 1; + ALARM_STATE_ARMED_AWAY = 2; + ALARM_STATE_ARMED_NIGHT = 3; + ALARM_STATE_ARMED_VACATION = 4; + ALARM_STATE_ARMED_CUSTOM_BYPASS = 5; + ALARM_STATE_PENDING = 6; + ALARM_STATE_ARMING = 7; + ALARM_STATE_DISARMING = 8; + ALARM_STATE_TRIGGERED = 9; +} + +enum AlarmControlPanelStateCommand { + ALARM_CONTROL_PANEL_DISARM = 0; + ALARM_CONTROL_PANEL_ARM_AWAY = 1; + ALARM_CONTROL_PANEL_ARM_HOME = 2; + ALARM_CONTROL_PANEL_ARM_NIGHT = 3; + ALARM_CONTROL_PANEL_ARM_VACATION = 4; + ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5; + ALARM_CONTROL_PANEL_TRIGGER = 6; +} + +message ListEntitiesAlarmControlPanelResponse { + option (id) = 94; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + uint32 supported_features = 8; + bool requires_code = 9; + bool requires_code_to_arm = 10; +} + +message AlarmControlPanelStateResponse { + option (id) = 95; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + option (no_delay) = true; + fixed32 key = 1; + AlarmControlPanelState state = 2; +} + +message AlarmControlPanelCommandRequest { + option (id) = 96; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + option (no_delay) = true; + fixed32 key = 1; + AlarmControlPanelStateCommand command = 2; + string code = 3; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 53e6ccd6dc..858ff0e525 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -931,6 +931,64 @@ void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventR #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + if (!this->state_subscription_) + return false; + + AlarmControlPanelStateResponse resp{}; + resp.key = a_alarm_control_panel->get_object_id_hash(); + resp.state = static_cast(a_alarm_control_panel->get_state()); + return this->send_alarm_control_panel_state_response(resp); +} +bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + ListEntitiesAlarmControlPanelResponse msg; + msg.key = a_alarm_control_panel->get_object_id_hash(); + msg.object_id = a_alarm_control_panel->get_object_id(); + msg.name = a_alarm_control_panel->get_name(); + msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel); + msg.icon = a_alarm_control_panel->get_icon(); + msg.disabled_by_default = a_alarm_control_panel->is_disabled_by_default(); + msg.entity_category = static_cast(a_alarm_control_panel->get_entity_category()); + msg.supported_features = a_alarm_control_panel->get_supported_features(); + msg.requires_code = a_alarm_control_panel->get_requires_code(); + msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); + return this->send_list_entities_alarm_control_panel_response(msg); +} +void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { + alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); + if (a_alarm_control_panel == nullptr) + return; + + auto call = a_alarm_control_panel->make_call(); + switch (msg.command) { + case enums::ALARM_CONTROL_PANEL_DISARM: + call.disarm(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_AWAY: + call.arm_away(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_HOME: + call.arm_home(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: + call.arm_night(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_VACATION: + call.arm_vacation(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: + call.arm_custom_bypass(); + break; + case enums::ALARM_CONTROL_PANEL_TRIGGER: + call.pending(); + break; + } + call.set_code(msg.code); + call.perform(); +} +#endif + bool APIConnection::send_log_message(int level, const char *tag, const char *line) { if (this->log_subscription_ < level) return false; diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 5398234f9f..c146adff02 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -129,6 +129,12 @@ class APIConnection : public APIServerConnection { void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; #endif +#ifdef USE_ALARM_CONTROL_PANEL + bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); + bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); + void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; +#endif + void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 019e653a7f..8c7f6d0c4a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -433,6 +433,57 @@ template<> const char *proto_enum_to_string(enums::V } } #endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) { + switch (value) { + case enums::ALARM_STATE_DISARMED: + return "ALARM_STATE_DISARMED"; + case enums::ALARM_STATE_ARMED_HOME: + return "ALARM_STATE_ARMED_HOME"; + case enums::ALARM_STATE_ARMED_AWAY: + return "ALARM_STATE_ARMED_AWAY"; + case enums::ALARM_STATE_ARMED_NIGHT: + return "ALARM_STATE_ARMED_NIGHT"; + case enums::ALARM_STATE_ARMED_VACATION: + return "ALARM_STATE_ARMED_VACATION"; + case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: + return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; + case enums::ALARM_STATE_PENDING: + return "ALARM_STATE_PENDING"; + case enums::ALARM_STATE_ARMING: + return "ALARM_STATE_ARMING"; + case enums::ALARM_STATE_DISARMING: + return "ALARM_STATE_DISARMING"; + case enums::ALARM_STATE_TRIGGERED: + return "ALARM_STATE_TRIGGERED"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> +const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) { + switch (value) { + case enums::ALARM_CONTROL_PANEL_DISARM: + return "ALARM_CONTROL_PANEL_DISARM"; + case enums::ALARM_CONTROL_PANEL_ARM_AWAY: + return "ALARM_CONTROL_PANEL_ARM_AWAY"; + case enums::ALARM_CONTROL_PANEL_ARM_HOME: + return "ALARM_CONTROL_PANEL_ARM_HOME"; + case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: + return "ALARM_CONTROL_PANEL_ARM_NIGHT"; + case enums::ALARM_CONTROL_PANEL_ARM_VACATION: + return "ALARM_CONTROL_PANEL_ARM_VACATION"; + case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: + return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; + case enums::ALARM_CONTROL_PANEL_TRIGGER: + return "ALARM_CONTROL_PANEL_TRIGGER"; + default: + return "UNKNOWN"; + } +} +#endif bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -6436,6 +6487,217 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + case 8: { + this->supported_features = value.as_uint32(); + return true; + } + case 9: { + this->requires_code = value.as_bool(); + return true; + } + case 10: { + this->requires_code_to_arm = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesAlarmControlPanelResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesAlarmControlPanelResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->supported_features); + buffer.encode_bool(9, this->requires_code); + buffer.encode_bool(10, this->requires_code_to_arm); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesAlarmControlPanelResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" supported_features: "); + sprintf(buffer, "%u", this->supported_features); + out.append(buffer); + out.append("\n"); + + out.append(" requires_code: "); + out.append(YESNO(this->requires_code)); + out.append("\n"); + + out.append(" requires_code_to_arm: "); + out.append(YESNO(this->requires_code_to_arm)); + out.append("\n"); + out.append("}"); +} +#endif +bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_enum(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void AlarmControlPanelStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AlarmControlPanelStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + out.append("}"); +} +#endif +bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->command = value.as_enum(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: { + this->code = value.as_string(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->command); + buffer.encode_string(3, this->code); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AlarmControlPanelCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" code: "); + out.append("'").append(this->code).append("'"); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 49307dfce0..769f7aaff5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -176,6 +176,27 @@ enum VoiceAssistantEvent : uint32_t { VOICE_ASSISTANT_TTS_START = 7, VOICE_ASSISTANT_TTS_END = 8, }; +enum AlarmControlPanelState : uint32_t { + ALARM_STATE_DISARMED = 0, + ALARM_STATE_ARMED_HOME = 1, + ALARM_STATE_ARMED_AWAY = 2, + ALARM_STATE_ARMED_NIGHT = 3, + ALARM_STATE_ARMED_VACATION = 4, + ALARM_STATE_ARMED_CUSTOM_BYPASS = 5, + ALARM_STATE_PENDING = 6, + ALARM_STATE_ARMING = 7, + ALARM_STATE_DISARMING = 8, + ALARM_STATE_TRIGGERED = 9, +}; +enum AlarmControlPanelStateCommand : uint32_t { + ALARM_CONTROL_PANEL_DISARM = 0, + ALARM_CONTROL_PANEL_ARM_AWAY = 1, + ALARM_CONTROL_PANEL_ARM_HOME = 2, + ALARM_CONTROL_PANEL_ARM_NIGHT = 3, + ALARM_CONTROL_PANEL_ARM_VACATION = 4, + ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5, + ALARM_CONTROL_PANEL_TRIGGER = 6, +}; } // namespace enums @@ -1680,6 +1701,56 @@ class VoiceAssistantEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + uint32_t supported_features{0}; + bool requires_code{false}; + bool requires_code_to_arm{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class AlarmControlPanelStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + enums::AlarmControlPanelState state{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class AlarmControlPanelCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + enums::AlarmControlPanelStateCommand command{}; + std::string code{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index e14e22cdc1..8752ae6cfd 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -476,6 +476,25 @@ bool APIServerConnectionBase::send_voice_assistant_request(const VoiceAssistantR #endif #ifdef USE_VOICE_ASSISTANT #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response( + const ListEntitiesAlarmControlPanelResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_alarm_control_panel_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 94); +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +bool APIServerConnectionBase::send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_alarm_control_panel_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 95); +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -883,6 +902,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_event_response(msg); +#endif + break; + } + case 96: { +#ifdef USE_ALARM_CONTROL_PANEL + AlarmControlPanelCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str()); +#endif + this->on_alarm_control_panel_command_request(msg); #endif break; } @@ -1295,6 +1325,19 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo this->subscribe_voice_assistant(msg); } #endif +#ifdef USE_ALARM_CONTROL_PANEL +void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->alarm_control_panel_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 8af7a53381..2864e303c0 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -239,6 +239,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_VOICE_ASSISTANT virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -324,6 +333,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_VOICE_ASSISTANT virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -405,6 +417,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_VOICE_ASSISTANT void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; #endif +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 27c82f7ccc..87b5f9e63f 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -338,5 +338,14 @@ void APIServer::stop_voice_assistant() { } #endif +#ifdef USE_ALARM_CONTROL_PANEL +void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_alarm_control_panel_state(obj); +} +#endif + } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 28e53f22e2..be124f42ff 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -85,6 +85,10 @@ class APIServer : public Component, public Controller { void stop_voice_assistant(); #endif +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; +#endif + bool is_connected() const; struct HomeAssistantStateSubscription { diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 85d4cd61ef..cd73d1ef5d 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -69,6 +69,11 @@ bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_play return this->client_->send_media_player_info(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + return this->client_->send_alarm_control_panel_info(a_alarm_control_panel); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 4fbaa509a2..b40d77e841 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -54,6 +54,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_MEDIA_PLAYER bool on_media_player(media_player::MediaPlayer *media_player) override; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 1d1ba0245e..66b5f40928 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -55,6 +55,11 @@ bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_play return this->client_->send_media_player_state(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); +} +#endif InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 7a7ba697c0..0597b9f384 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -51,6 +51,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_MEDIA_PLAYER bool on_media_player(media_player::MediaPlayer *media_player) override; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py new file mode 100644 index 0000000000..5156a0832a --- /dev/null +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -0,0 +1,123 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ( + binary_sensor, + alarm_control_panel, +) +from esphome.const import ( + CONF_ID, + CONF_BINARY_SENSORS, + CONF_INPUT, + CONF_RESTORE_MODE, +) +from .. import template_ns + +CODEOWNERS = ["@grahambrown11"] + +CONF_CODES = "codes" +CONF_BYPASS_ARMED_HOME = "bypass_armed_home" +CONF_REQUIRES_CODE_TO_ARM = "requires_code_to_arm" +CONF_ARMING_HOME_TIME = "arming_home_time" +CONF_ARMING_AWAY_TIME = "arming_away_time" +CONF_PENDING_TIME = "pending_time" +CONF_TRIGGER_TIME = "trigger_time" + +FLAG_NORMAL = "normal" +FLAG_BYPASS_ARMED_HOME = "bypass_armed_home" + +BinarySensorFlags = { + FLAG_NORMAL: 1 << 0, + FLAG_BYPASS_ARMED_HOME: 1 << 1, +} + +TemplateAlarmControlPanel = template_ns.class_( + "TemplateAlarmControlPanel", alarm_control_panel.AlarmControlPanel, cg.Component +) + +TemplateAlarmControlPanelRestoreMode = template_ns.enum( + "TemplateAlarmControlPanelRestoreMode" +) +RESTORE_MODES = { + "ALWAYS_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_ALWAYS_DISARMED, + "RESTORE_DEFAULT_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, +} + + +def validate_config(config): + if config.get(CONF_REQUIRES_CODE_TO_ARM, False) and not config.get(CONF_CODES, []): + raise cv.Invalid( + f"{CONF_REQUIRES_CODE_TO_ARM} cannot be True when there are no codes." + ) + return config + + +TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_INPUT): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_BYPASS_ARMED_HOME, default=False): cv.boolean, + }, + key=CONF_INPUT, +) + +TEMPLATE_ALARM_CONTROL_PANEL_SCHEMA = ( + alarm_control_panel.ALARM_CONTROL_PANEL_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateAlarmControlPanel), + cv.Optional(CONF_CODES): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_REQUIRES_CODE_TO_ARM): cv.boolean, + cv.Optional(CONF_ARMING_HOME_TIME): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_ARMING_AWAY_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_PENDING_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TRIGGER_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( + TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA + ), + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_DISARMED"): cv.enum( + RESTORE_MODES, upper=True + ), + } + ).extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.All( + TEMPLATE_ALARM_CONTROL_PANEL_SCHEMA, + validate_config, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await alarm_control_panel.register_alarm_control_panel(var, config) + if CONF_CODES in config: + for acode in config[CONF_CODES]: + cg.add(var.add_code(acode)) + if CONF_REQUIRES_CODE_TO_ARM in config: + cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM])) + + cg.add(var.set_arming_away_time(config[CONF_ARMING_AWAY_TIME])) + cg.add(var.set_pending_time(config[CONF_PENDING_TIME])) + cg.add(var.set_trigger_time(config[CONF_TRIGGER_TIME])) + + supports_arm_home = False + if CONF_ARMING_HOME_TIME in config: + cg.add(var.set_arming_home_time(config[CONF_ARMING_HOME_TIME])) + supports_arm_home = True + + for sensor in config.get(CONF_BINARY_SENSORS, []): + bs = await cg.get_variable(sensor[CONF_INPUT]) + flags = BinarySensorFlags[FLAG_NORMAL] + if sensor[CONF_BYPASS_ARMED_HOME]: + flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_HOME] + supports_arm_home = True + cg.add(var.add_sensor(bs, flags)) + + cg.add(var.set_supports_arm_home(supports_arm_home)) + + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp new file mode 100644 index 0000000000..1c54998e42 --- /dev/null +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -0,0 +1,180 @@ +#include "template_alarm_control_panel.h" +#include +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +using namespace esphome::alarm_control_panel; + +static const char *const TAG = "template.alarm_control_panel"; + +TemplateAlarmControlPanel::TemplateAlarmControlPanel(){}; + +#ifdef USE_BINARY_SENSOR +void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags) { + this->sensor_map_[sensor] = flags; +}; +#endif + +void TemplateAlarmControlPanel::dump_config() { + ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:"); + ESP_LOGCONFIG(TAG, " Current State: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_))); + ESP_LOGCONFIG(TAG, " Number of Codes: %u", this->codes_.size()); + if (!this->codes_.empty()) + ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->requires_code_to_arm_)); + ESP_LOGCONFIG(TAG, " Arming Away Time: %us", (this->arming_away_time_ / 1000)); + if (this->arming_home_time_ != 0) + ESP_LOGCONFIG(TAG, " Arming Home Time: %us", (this->arming_home_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Pending Time: %us", (this->pending_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Trigger Time: %us", (this->trigger_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Supported Features: %u", this->get_supported_features()); +#ifdef USE_BINARY_SENSOR + for (auto sensor_pair : this->sensor_map_) { + ESP_LOGCONFIG(TAG, " Binary Sesnsor:"); + ESP_LOGCONFIG(TAG, " Name: %s", sensor_pair.first->get_name().c_str()); + ESP_LOGCONFIG(TAG, " Armed home bypass: %s", + TRUEFALSE(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)); + } +#endif +} + +void TemplateAlarmControlPanel::setup() { + ESP_LOGCONFIG(TAG, "Setting up Template AlarmControlPanel '%s'...", this->name_.c_str()); + switch (this->restore_mode_) { + case ALARM_CONTROL_PANEL_ALWAYS_DISARMED: + this->current_state_ = ACP_STATE_DISARMED; + break; + case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: { + uint8_t value; + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (this->pref_.load(&value)) { + this->current_state_ = static_cast(value); + } else { + this->current_state_ = ACP_STATE_DISARMED; + } + break; + } + } + this->desired_state_ = this->current_state_; +} + +void TemplateAlarmControlPanel::loop() { + // change from ARMING to ARMED_x after the arming_time_ has passed + if (this->current_state_ == ACP_STATE_ARMING) { + auto delay = this->arming_away_time_; + if (this->desired_state_ == ACP_STATE_ARMED_HOME) { + delay = this->arming_home_time_; + } + if ((millis() - this->last_update_) > delay) { + this->publish_state(this->desired_state_); + } + return; + } + // change from PENDING to TRIGGERED after the delay_time_ has passed + if (this->current_state_ == ACP_STATE_PENDING && (millis() - this->last_update_) > this->pending_time_) { + this->publish_state(ACP_STATE_TRIGGERED); + return; + } + auto future_state = this->current_state_; + // reset triggered if all clear + if (this->current_state_ == ACP_STATE_TRIGGERED && this->trigger_time_ > 0 && + (millis() - this->last_update_) > this->trigger_time_) { + future_state = this->desired_state_; + } + bool trigger = false; +#ifdef USE_BINARY_SENSOR + if (this->is_state_armed(future_state)) { + // TODO might be better to register change for each sensor in setup... + for (auto sensor_pair : this->sensor_map_) { + if (sensor_pair.first->state) { + if (this->current_state_ == ACP_STATE_ARMED_HOME && + (sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { + continue; + } + trigger = true; + break; + } + } + } +#endif + if (trigger) { + if (this->pending_time_ > 0 && this->current_state_ != ACP_STATE_TRIGGERED) { + this->publish_state(ACP_STATE_PENDING); + } else { + this->publish_state(ACP_STATE_TRIGGERED); + } + } else if (future_state != this->current_state_) { + this->publish_state(future_state); + } +} + +bool TemplateAlarmControlPanel::is_code_valid_(optional code) { + if (!this->codes_.empty()) { + if (code.has_value()) { + ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str()); + return (std::count(this->codes_.begin(), this->codes_.end(), code.value()) == 1); + } + ESP_LOGD(TAG, "No code provided"); + return false; + } + return true; +} + +uint32_t TemplateAlarmControlPanel::get_supported_features() const { + uint32_t features = ACP_FEAT_ARM_AWAY | ACP_FEAT_TRIGGER; + if (this->supports_arm_home_) { + features |= ACP_FEAT_ARM_HOME; + } + return features; +} + +bool TemplateAlarmControlPanel::get_requires_code() const { return !this->codes_.empty(); } + +void TemplateAlarmControlPanel::arm_(optional code, alarm_control_panel::AlarmControlPanelState state, + uint32_t delay) { + if (this->current_state_ != ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot arm when not disarmed"); + return; + } + if (this->requires_code_to_arm_ && !this->is_code_valid_(std::move(code))) { + ESP_LOGW(TAG, "Not arming code doesn't match"); + return; + } + this->desired_state_ = state; + if (delay > 0) { + this->publish_state(ACP_STATE_ARMING); + } else { + this->publish_state(state); + } +} + +void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { + if (call.get_state()) { + if (call.get_state() == ACP_STATE_ARMED_AWAY) { + this->arm_(call.get_code(), ACP_STATE_ARMED_AWAY, this->arming_away_time_); + } else if (call.get_state() == ACP_STATE_ARMED_HOME) { + this->arm_(call.get_code(), ACP_STATE_ARMED_HOME, this->arming_home_time_); + } else if (call.get_state() == ACP_STATE_DISARMED) { + if (!this->is_code_valid_(call.get_code())) { + ESP_LOGW(TAG, "Not disarming code doesn't match"); + return; + } + this->desired_state_ = ACP_STATE_DISARMED; + this->publish_state(ACP_STATE_DISARMED); + } else if (call.get_state() == ACP_STATE_TRIGGERED) { + this->publish_state(ACP_STATE_TRIGGERED); + } else if (call.get_state() == ACP_STATE_PENDING) { + this->publish_state(ACP_STATE_PENDING); + } else { + ESP_LOGE(TAG, "State not yet implemented: %s", + LOG_STR_ARG(alarm_control_panel_state_to_string(*call.get_state()))); + } + } +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h new file mode 100644 index 0000000000..4065356ba8 --- /dev/null +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -0,0 +1,116 @@ +#pragma once + +#include + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" + +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +namespace esphome { +namespace template_ { + +#ifdef USE_BINARY_SENSOR +enum BinarySensorFlags : uint16_t { + BINARY_SENSOR_MODE_NORMAL = 1 << 0, + BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1, +}; +#endif + +enum TemplateAlarmControlPanelRestoreMode { + ALARM_CONTROL_PANEL_ALWAYS_DISARMED, + ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, +}; + +class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { + public: + TemplateAlarmControlPanel(); + void dump_config() override; + void setup() override; + void loop() override; + uint32_t get_supported_features() const override; + bool get_requires_code() const override; + bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } + void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + +#ifdef USE_BINARY_SENSOR + /** Add a binary_sensor to the alarm_panel. + * + * @param sensor The BinarySensor instance. + * @param ignore_when_home if this should be ignored when armed_home mode + */ + void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0); +#endif + + /** add a code + * + * @param code The code + */ + void add_code(const std::string &code) { this->codes_.push_back(code); } + + /** set requires a code to arm + * + * @param code_to_arm The requires code to arm + */ + void set_requires_code_to_arm(bool code_to_arm) { this->requires_code_to_arm_ = code_to_arm; } + + /** set the delay before arming away + * + * @param time The milliseconds + */ + void set_arming_away_time(uint32_t time) { this->arming_away_time_ = time; } + + /** set the delay before arming home + * + * @param time The milliseconds + */ + void set_arming_home_time(uint32_t time) { this->arming_home_time_ = time; } + + /** set the delay before triggering + * + * @param time The milliseconds + */ + void set_pending_time(uint32_t time) { this->pending_time_ = time; } + + /** set the delay before resetting after triggered + * + * @param time The milliseconds + */ + void set_trigger_time(uint32_t time) { this->trigger_time_ = time; } + + void set_supports_arm_home(bool supports_arm_home) { supports_arm_home_ = supports_arm_home; } + + protected: + void control(const alarm_control_panel::AlarmControlPanelCall &call) override; +#ifdef USE_BINARY_SENSOR + // the map of binary sensors that the alarm_panel monitors with their modes + std::map sensor_map_; +#endif + TemplateAlarmControlPanelRestoreMode restore_mode_{}; + + // the arming away delay + uint32_t arming_away_time_; + // the arming home delay + uint32_t arming_home_time_{0}; + // the trigger delay + uint32_t pending_time_; + // the time in trigger + uint32_t trigger_time_; + // a list of codes + std::vector codes_; + // requires a code to arm + bool requires_code_to_arm_ = false; + bool supports_arm_home_ = false; + // check if the code is valid + bool is_code_valid_(optional code); + + void arm_(optional code, alarm_control_panel::AlarmControlPanelState state, uint32_t delay); +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 6f833a5c83..ce7b4be7f3 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -91,6 +91,16 @@ bool ListEntitiesIterator::on_select(select::Select *select) { } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + this->web_server_->events_.send( + this->web_server_->alarm_control_panel_json(a_alarm_control_panel, a_alarm_control_panel->get_state(), DETAIL_ALL) + .c_str(), + "state"); + return true; +} +#endif + } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 85868caff8..8ddca15edf 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -49,6 +49,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; #endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; +#endif protected: WebServer *web_server_; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 00b2e20015..1ac94375c2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -813,6 +813,9 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value } #endif +// Longest: HORIZONTAL +#define PSTR_LOCAL(mode_s) strncpy_P(buf, (PGM_P) ((mode_s)), 15) + #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { this->events_.send(this->climate_json(obj, DETAIL_STATE).c_str(), "state"); @@ -869,16 +872,13 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url request->send(404); } -// Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) strncpy_P(__buf, (PGM_P) ((mode_s)), 15) - std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { return json::build_json([obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); - char __buf[16]; + char buf[16]; if (start_config == DETAIL_ALL) { JsonArray opt = root.createNestedArray("modes"); @@ -996,6 +996,34 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } #endif +#ifdef USE_ALARM_CONTROL_PANEL +void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + this->events_.send(this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE).c_str(), "state"); +} +std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { + char buf[16]; + set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), + PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); + }); +} +void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE); + request->send(200, "application/json", data.c_str()); + return; + } + } + request->send(404); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -1073,6 +1101,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_ALARM_CONTROL_PANEL + if (request->method() == HTTP_GET && match.domain == "alarm_control_panel") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -1180,6 +1213,14 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_ALARM_CONTROL_PANEL + if (match.domain == "alarm_control_panel") { + this->handle_alarm_control_panel_request(request, match); + + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index f4122ef754..45d0bc03a4 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -216,6 +216,17 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; + + /// Handle a alarm_control_panel request under '/alarm_control_panel/'. + void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the alarm_control_panel state with its value as a JSON string. + std::string alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. diff --git a/esphome/core/application.h b/esphome/core/application.h index 0992a4df39..0501d1a56a 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -48,6 +48,9 @@ #ifdef USE_MEDIA_PLAYER #include "esphome/components/media_player/media_player.h" #endif +#ifdef USE_ALARM_CONTROL_PANEL +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#endif namespace esphome { @@ -126,6 +129,12 @@ class Application { void register_media_player(media_player::MediaPlayer *media_player) { this->media_players_.push_back(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL + void register_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + this->alarm_control_panels_.push_back(a_alarm_control_panel); + } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -296,6 +305,18 @@ class Application { } #endif +#ifdef USE_ALARM_CONTROL_PANEL + const std::vector &get_alarm_control_panels() { + return this->alarm_control_panels_; + } + alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->alarm_control_panels_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif + Scheduler scheduler; protected: @@ -349,6 +370,9 @@ class Application { #ifdef USE_MEDIA_PLAYER std::vector media_players_{}; #endif +#ifdef USE_ALARM_CONTROL_PANEL + std::vector alarm_control_panels_{}; +#endif std::string name_; std::string friendly_name_; diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 16c56a90c5..871f6d6c0e 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -246,6 +246,21 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + case IteratorState::ALARM_CONTROL_PANEL: + if (this->at_ >= App.get_alarm_control_panels().size()) { + advance_platform = true; + } else { + auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_]; + if (a_alarm_control_panel->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_alarm_control_panel(a_alarm_control_panel); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index e8e048bd73..8b2da6218c 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -65,6 +65,9 @@ class ComponentIterator { #endif #ifdef USE_MEDIA_PLAYER virtual bool on_media_player(media_player::MediaPlayer *media_player); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) = 0; #endif virtual bool on_end(); @@ -116,6 +119,9 @@ class ComponentIterator { #endif #ifdef USE_MEDIA_PLAYER MEDIA_PLAYER, +#endif +#ifdef USE_ALARM_CONTROL_PANEL + ALARM_CONTROL_PANEL, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 857ee6398f..2ce471ead0 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -79,6 +79,12 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); }); } #endif +#ifdef USE_ALARM_CONTROL_PANEL + for (auto *obj : App.get_alarm_control_panels()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 0966504c43..25a4acb36e 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -40,6 +40,9 @@ #ifdef USE_MEDIA_PLAYER #include "esphome/components/media_player/media_player.h" #endif +#ifdef USE_ALARM_CONTROL_PANEL +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#endif namespace esphome { @@ -82,6 +85,9 @@ class Controller { #ifdef USE_MEDIA_PLAYER virtual void on_media_player_update(media_player::MediaPlayer *obj){}; #endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 64edabc878..638dd39364 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -17,6 +17,7 @@ #define USE_API #define USE_API_NOISE #define USE_API_PLAINTEXT +#define USE_ALARM_CONTROL_PANEL #define USE_BINARY_SENSOR #define USE_BUTTON #define USE_CLIMATE diff --git a/tests/test1.yaml b/tests/test1.yaml index bee0d93faf..f8928430f4 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3371,3 +3371,21 @@ lcd_menu: on_prev: then: lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - binary_sensor1 + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())); diff --git a/tests/test3.yaml b/tests/test3.yaml index c4847725e8..8307ac2984 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -822,7 +822,6 @@ switch: name: R0 Switch component_name: page0.r0 - climate: - platform: bang_bang name: Bang Bang Climate @@ -1106,7 +1105,6 @@ rf_bridge: - rf_bridge.send_raw: raw: "AAA5070008001000ABC12355" - display: - platform: nextion uart_id: uart_1 @@ -1171,3 +1169,22 @@ daly_bms: qr_code: - id: homepage_qr value: https://esphome.io/index.html + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state()));