diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b3fa6d4932..ab4f8cc960 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -40,6 +40,7 @@ "yaml.customTags": [ "!secret scalar", "!lambda scalar", + "!extend scalar", "!include_dir_named scalar", "!include_dir_list scalar", "!include_dir_merge_list scalar", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 307dd496f0..acf1f29410 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -36,6 +36,24 @@ ] } ] + }, + { + "label": "Generate proto files", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "./script/api_protobuf/api_protobuf.py" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "never", + "close": true, + "panel": "new" + }, + "problemMatcher": [] } ] } diff --git a/CODEOWNERS b/CODEOWNERS index 94196be02c..cef045468c 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 @@ -278,6 +279,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/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 1be2950d68..c8dc7b62e2 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -15,6 +15,25 @@ static const char *const TAG = "display"; const Color COLOR_OFF(0, 0, 0, 255); const Color COLOR_ON(255, 255, 255, 255); +static int image_type_to_bpp(ImageType type) { + switch (type) { + case IMAGE_TYPE_BINARY: + return 1; + case IMAGE_TYPE_GRAYSCALE: + return 8; + case IMAGE_TYPE_RGB565: + return 16; + case IMAGE_TYPE_RGB24: + return 24; + case IMAGE_TYPE_RGBA: + return 32; + default: + return 0; + } +} + +static int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; } + void Rect::expand(int16_t horizontal, int16_t vertical) { if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { this->x = this->x - horizontal; @@ -306,63 +325,8 @@ void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign al this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { - bool transparent = image->has_transparency(); - - switch (image->get_type()) { - case IMAGE_TYPE_BINARY: { - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - if (image->get_pixel(img_x, img_y)) { - this->draw_pixel_at(x + img_x, y + img_y, color_on); - } else if (!transparent) { - this->draw_pixel_at(x + img_x, y + img_y, color_off); - } - } - } - break; - } - case IMAGE_TYPE_GRAYSCALE: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - auto color = image->get_grayscale_pixel(img_x, img_y); - if (color.w >= 0x80) { - this->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - case IMAGE_TYPE_RGB565: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - auto color = image->get_rgb565_pixel(img_x, img_y); - if (color.w >= 0x80) { - this->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - case IMAGE_TYPE_RGB24: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - auto color = image->get_color_pixel(img_x, img_y); - if (color.w >= 0x80) { - this->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - case IMAGE_TYPE_RGBA: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - auto color = image->get_rgba_pixel(img_x, img_y); - if (color.w >= 0x80) { - this->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - } +void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { + image->draw(x, y, this, color_on, color_off); } #ifdef USE_GRAPH @@ -637,23 +601,91 @@ Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : basel glyphs_.emplace_back(&data[i]); } -bool Image::get_pixel(int x, int y) const { +void Image::draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) { + switch (type_) { + case IMAGE_TYPE_BINARY: { + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + if (this->get_binary_pixel_(img_x, img_y)) { + display->draw_pixel_at(x + img_x, y + img_y, color_on); + } else if (!this->transparent_) { + display->draw_pixel_at(x + img_x, y + img_y, color_off); + } + } + } + break; + } + case IMAGE_TYPE_GRAYSCALE: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_grayscale_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGB565: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgb565_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGB24: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgb24_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGBA: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgba_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + } +} +Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return false; + return color_off; + switch (this->type_) { + case IMAGE_TYPE_BINARY: + return this->get_binary_pixel_(x, y) ? color_on : color_off; + case IMAGE_TYPE_GRAYSCALE: + return this->get_grayscale_pixel_(x, y); + case IMAGE_TYPE_RGB565: + return this->get_rgb565_pixel_(x, y); + case IMAGE_TYPE_RGB24: + return this->get_rgb24_pixel_(x, y); + case IMAGE_TYPE_RGBA: + return this->get_rgba_pixel_(x, y); + default: + return color_off; + } +} +bool Image::get_binary_pixel_(int x, int y) const { const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; const uint32_t pos = x + y * width_8; return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } -Color Image::get_rgba_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; +Color Image::get_rgba_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_) * 4; return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); } -Color Image::get_color_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; +Color Image::get_rgb24_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_) * 3; Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), progmem_read_byte(this->data_start_ + pos + 2)); @@ -666,9 +698,7 @@ Color Image::get_color_pixel(int x, int y) const { } return color; } -Color Image::get_rgb565_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; +Color Image::get_rgb565_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_) * 2; uint16_t rgb565 = progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); @@ -684,9 +714,7 @@ Color Image::get_rgb565_pixel(int x, int y) const { } return color; } -Color Image::get_grayscale_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; +Color Image::get_grayscale_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_); const uint8_t gray = progmem_read_byte(this->data_start_ + pos); uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; @@ -697,80 +725,10 @@ int Image::get_height() const { return this->height_; } ImageType Image::get_type() const { return this->type_; } Image::Image(const uint8_t *data_start, int width, int height, ImageType type) : width_(width), height_(height), type_(type), data_start_(data_start) {} -int Image::get_current_frame() const { return 0; } -bool Animation::get_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return false; - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t frame_index = this->height_ * width_8 * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return false; - const uint32_t pos = x + y * width_8 + frame_index; - return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); -} -Color Animation::get_rgba_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 4; - return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); -} -Color Animation::get_color_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 3; - Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2)); - if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { - // (0, 0, 1) has been defined as transparent color for non-alpha images. - // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) - color.w = 0; - } else { - color.w = 0xFF; - } - return color; -} -Color Animation::get_rgb565_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 2; - uint16_t rgb565 = - progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); - auto r = (rgb565 & 0xF800) >> 11; - auto g = (rgb565 & 0x07E0) >> 5; - auto b = rgb565 & 0x001F; - Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); - if (rgb565 == 0x0020 && transparent_) { - // darkest green has been defined as transparent color for transparent RGB565 images. - color.w = 0; - } else { - color.w = 0xFF; - } - return color; -} -Color Animation::get_grayscale_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index); - const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; - return Color(gray, gray, gray, alpha); -} Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) : Image(data_start, width, height, type), + animation_data_start_(data_start), current_frame_(0), animation_frame_count_(animation_frame_count), loop_start_frame_(0), @@ -797,12 +755,16 @@ void Animation::next_frame() { this->loop_current_iteration_ = 1; this->current_frame_ = 0; } + + this->update_data_start_(); } void Animation::prev_frame() { this->current_frame_--; if (this->current_frame_ < 0) { this->current_frame_ = this->animation_frame_count_ - 1; } + + this->update_data_start_(); } void Animation::set_frame(int frame) { @@ -815,6 +777,13 @@ void Animation::set_frame(int frame) { this->current_frame_ = this->animation_frame_count_ - abs_frame; } } + + this->update_data_start_(); +} + +void Animation::update_data_start_() { + const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_; + this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; } DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index bcfe75f6b4..0c31ac24d9 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -123,8 +123,8 @@ class Rect { void info(const std::string &prefix = "rect info:"); }; +class BaseImage; class Font; -class Image; class DisplayBuffer; class DisplayPage; class DisplayOnPageChangeTrigger; @@ -315,7 +315,7 @@ class DisplayBuffer { * @param color_on The color to replace in binary images for the on bits. * @param color_off The color to replace in binary images for the off bits. */ - void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); #ifdef USE_GRAPH /** Draw the `graph` with the top-left corner at [x,y] to the screen. @@ -529,24 +529,33 @@ class Font { int height_; }; -class Image { +class BaseImage { + public: + virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0; + virtual int get_width() const = 0; + virtual int get_height() const = 0; +}; + +class Image : public BaseImage { public: Image(const uint8_t *data_start, int width, int height, ImageType type); - virtual bool get_pixel(int x, int y) const; - virtual Color get_color_pixel(int x, int y) const; - virtual Color get_rgba_pixel(int x, int y) const; - virtual Color get_rgb565_pixel(int x, int y) const; - virtual Color get_grayscale_pixel(int x, int y) const; - int get_width() const; - int get_height() const; + Color get_pixel(int x, int y, Color color_on = COLOR_ON, Color color_off = COLOR_OFF) const; + int get_width() const override; + int get_height() const override; ImageType get_type() const; - virtual int get_current_frame() const; + void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) override; void set_transparency(bool transparent) { transparent_ = transparent; } bool has_transparency() const { return transparent_; } protected: + bool get_binary_pixel_(int x, int y) const; + Color get_rgb24_pixel_(int x, int y) const; + Color get_rgba_pixel_(int x, int y) const; + Color get_rgb565_pixel_(int x, int y) const; + Color get_grayscale_pixel_(int x, int y) const; + int width_; int height_; ImageType type_; @@ -557,14 +566,9 @@ class Image { class Animation : public Image { public: Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); - bool get_pixel(int x, int y) const override; - Color get_color_pixel(int x, int y) const override; - Color get_rgba_pixel(int x, int y) const override; - Color get_rgb565_pixel(int x, int y) const override; - Color get_grayscale_pixel(int x, int y) const override; uint32_t get_animation_frame_count() const; - int get_current_frame() const override; + int get_current_frame() const; void next_frame(); void prev_frame(); @@ -577,6 +581,9 @@ class Animation : public Image { void set_loop(uint32_t start_frame, uint32_t end_frame, int count); protected: + void update_data_start_(); + + const uint8_t *animation_data_start_; int current_frame_; uint32_t animation_frame_count_; uint32_t loop_start_frame_; diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index dfc3fb2be2..600a308e6c 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -27,6 +27,7 @@ i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") CONF_MUTE_PIN = "mute_pin" CONF_AUDIO_ID = "audio_id" CONF_DAC_TYPE = "dac_type" +CONF_I2S_COMM_FMT = "i2s_comm_fmt" INTERNAL_DAC_OPTIONS = { "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, @@ -38,6 +39,8 @@ EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] +I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] + def validate_esp32_variant(config): if config[CONF_DAC_TYPE] != "internal": @@ -69,6 +72,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MODE, default="mono"): cv.one_of( *EXTERNAL_DAC_OPTIONS, lower=True ), + cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of( + *I2C_COMM_FMT_OPTIONS, lower=True + ), } ).extend(cv.COMPONENT_SCHEMA), }, @@ -94,6 +100,7 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) cg.add(var.set_mute_pin(pin)) cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) + cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp index 6eaa32c23c..9e2e3f136a 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -148,6 +148,7 @@ void I2SAudioMediaPlayer::start_() { pin_config.data_out_num = this->dout_pin_; i2s_set_pin(this->parent_->get_port(), &pin_config); + this->audio_->setI2SCommFMT_LSB(this->i2s_comm_fmt_lsb_); this->audio_->forceMono(this->external_dac_channels_ == 1); if (this->mute_pin_ != nullptr) { this->mute_pin_->setup(); diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h index dab9a85d7c..092e6de8e8 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h @@ -39,6 +39,8 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, #endif void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } + void set_i2s_comm_fmt_lsb(bool lsb) { this->i2s_comm_fmt_lsb_ = lsb; } + media_player::MediaPlayerTraits get_traits() override; bool is_muted() const override { return this->muted_; } @@ -71,6 +73,8 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, #endif uint8_t external_dac_channels_; + bool i2s_comm_fmt_lsb_; + HighFrequencyLoopRequester high_freq_; optional current_url_{}; diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 780c64ec70..98edadb6a5 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -44,6 +44,7 @@ MODELS = { "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), + "S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI), } COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 67d643fe31..ad70bd6e48 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -421,5 +421,17 @@ void ILI9XXXST7796::initialize() { } } +// 24_TFT rotated display +void ILI9XXXS3BoxLite::initialize() { + this->init_lcd_(INITCMD_S3BOXLITE); + if (this->width_ == 0) { + this->width_ = 320; + } + if (this->height_ == 0) { + this->height_ = 240; + } + this->invert_display_(true); +} + } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 8a8cd4bb44..dbdf023bc0 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -134,5 +134,10 @@ class ILI9XXXST7796 : public ILI9XXXDisplay { void initialize() override; }; +class ILI9XXXS3BoxLite : public ILI9XXXDisplay { + protected: + void initialize() override; +}; + } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 593b9a79ce..36cc30ec2e 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -169,6 +169,36 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = { 0x00 // End of list }; +static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9XXX_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9XXX_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9XXX_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9XXX_VMCTR2 , 1, 0x86, // VCM control2 + ILI9XXX_MADCTL , 1, 0x40, // Memory Access Control + ILI9XXX_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9XXX_PIXFMT , 1, 0x55, + ILI9XXX_FRMCTR1 , 2, 0x00, 0x18, + ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x0E, 0x09, 0x00, + ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0x36, 0x0F, + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + // clang-format on } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 113c5a2df1..e7cf492c7b 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -199,6 +199,27 @@ IMAGE_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) +def load_svg_image(file: str, resize: tuple[int, int]): + from PIL import Image + + # This import is only needed in case of SVG images; adding it + # to the top would force configurations not using SVG to also have it + # installed for no reason. + from cairosvg import svg2png + + if resize: + req_width, req_height = resize + svg_image = svg2png( + url=file, + output_width=req_width, + output_height=req_height, + ) + else: + svg_image = svg2png(url=file) + + return Image.open(io.BytesIO(svg_image)) + + async def to_code(config): from PIL import Image @@ -206,30 +227,20 @@ async def to_code(config): if conf_file[CONF_SOURCE] == SOURCE_LOCAL: path = CORE.relative_config_path(conf_file[CONF_PATH]) - try: - image = Image.open(path) - except Exception as e: - raise core.EsphomeError(f"Could not load image file {path}: {e}") - if CONF_RESIZE in config: - image.thumbnail(config[CONF_RESIZE]) + elif conf_file[CONF_SOURCE] == SOURCE_MDI: - # Those imports are only needed in case of MDI images; adding them - # to the top would force configurations not using MDI to also have them - # installed for no reason. - from cairosvg import svg2png + path = _compute_local_icon_path(conf_file).as_posix() - svg_file = _compute_local_icon_path(conf_file) - if CONF_RESIZE in config: - req_width, req_height = config[CONF_RESIZE] - svg_image = svg2png( - url=svg_file.as_posix(), - output_width=req_width, - output_height=req_height, - ) + try: + resize = config.get(CONF_RESIZE) + if path.lower().endswith(".svg"): + image = load_svg_image(path, resize) else: - svg_image = svg2png(url=svg_file.as_posix()) - - image = Image.open(io.BytesIO(svg_image)) + image = Image.open(path) + if resize: + image.thumbnail(resize) + except Exception as e: + raise core.EsphomeError(f"Could not load image file {path}: {e}") width, height = image.size diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 7d21032b8a..ec9970d1a0 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -278,7 +278,9 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } } else { - b = pixels[7 - col]; + for (uint8_t i = 0; i < 8; i++) { + b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + } } // send this byte to display at selected chip if (this->invert_) { diff --git a/esphome/components/pn532_i2c/__init__.py b/esphome/components/pn532_i2c/__init__.py index 36af2f8aa0..f7b8743967 100644 --- a/esphome/components/pn532_i2c/__init__.py +++ b/esphome/components/pn532_i2c/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID AUTO_LOAD = ["pn532"] CODEOWNERS = ["@OttoWinter", "@jesserockz"] DEPENDENCIES = ["i2c"] +MULTI_CONF = True pn532_i2c_ns = cg.esphome_ns.namespace("pn532_i2c") PN532I2C = pn532_i2c_ns.class_("PN532I2C", pn532.PN532, i2c.I2CDevice) 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/vbus/__init__.py b/esphome/components/vbus/__init__.py index 70f130e23b..99a473a3dc 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -15,6 +15,7 @@ VBus = vbus_ns.class_("VBus", uart.UARTDevice, cg.Component) CONF_VBUS_ID = "vbus_id" CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus" +CONF_DELTASOL_BS_2009 = "deltasol_bs_2009" CONF_DELTASOL_C = "deltasol_c" CONF_DELTASOL_CS2 = "deltasol_cs2" CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py index 9901fb2724..70fbda2d1f 100644 --- a/esphome/components/vbus/binary_sensor/__init__.py +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -18,12 +18,14 @@ from .. import ( VBus, CONF_VBUS_ID, CONF_DELTASOL_BS_PLUS, + CONF_DELTASOL_BS_2009, CONF_DELTASOL_C, CONF_DELTASOL_CS2, CONF_DELTASOL_CS_PLUS, ) DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component) +DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) @@ -42,6 +44,7 @@ CONF_COLLECTOR_FROST = "collector_frost" CONF_TUBE_COLLECTOR = "tube_collector" CONF_RECOOLING = "recooling" CONF_HQM = "hqm" +CONF_FROST_PROTECTION_ACTIVE = "frost_protection_active" CONFIG_SCHEMA = cv.typed_schema( { @@ -87,6 +90,33 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS_2009: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS_2009), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional( + CONF_FROST_PROTECTION_ACTIVE + ): binary_sensor.binary_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -222,6 +252,28 @@ async def to_code(config): sens = await binary_sensor.new_binary_sensor(config[CONF_HQM]) cg.add(var.set_hqm_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS_2009: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x427B)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + if CONF_FROST_PROTECTION_ACTIVE in config: + sens = await binary_sensor.new_binary_sensor( + config[CONF_FROST_PROTECTION_ACTIVE] + ) + cg.add(var.set_frost_protection_active_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp index 6edbae22ba..087d049a57 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -50,6 +50,28 @@ void DeltaSolBSPlusBSensor::handle_message(std::vector &message) { this->hqm_bsensor_->publish_state(message[15] & 0x20); } +void DeltaSolBS2009BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol BS 2009:"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Frost Protection Active", this->frost_protection_active_bsensor_); +} + +void DeltaSolBS2009BSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[20] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[20] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[20] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[20] & 8); + if (this->frost_protection_active_bsensor_ != nullptr) + this->frost_protection_active_bsensor_->publish_state(message[25] & 1); +} + void DeltaSolCBSensor::dump_config() { ESP_LOGCONFIG(TAG, "Deltasol C:"); LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index c0a823a0ab..146aa1b673 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -39,6 +39,27 @@ class DeltaSolBSPlusBSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; +class DeltaSolBS2009BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + void set_frost_protection_active_bsensor(binary_sensor::BinarySensor *bsensor) { + this->frost_protection_active_bsensor_ = bsensor; + } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *frost_protection_active_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + class DeltaSolCBSensor : public VBusListener, public Component { public: void dump_config() override; diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index bce28758ce..2ad9da424e 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -33,12 +33,14 @@ from .. import ( VBus, CONF_VBUS_ID, CONF_DELTASOL_BS_PLUS, + CONF_DELTASOL_BS_2009, CONF_DELTASOL_C, CONF_DELTASOL_CS2, CONF_DELTASOL_CS_PLUS, ) DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component) +DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) @@ -142,6 +144,87 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS_2009: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS_2009), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -437,6 +520,44 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_VERSION]) cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS_2009: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x427B)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_TIME in config: + sens = await sensor.new_sensor(config[CONF_TIME]) + cg.add(var.set_time_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index 8261773431..5b4f57f73d 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -57,6 +57,47 @@ void DeltaSolBSPlusSensor::handle_message(std::vector &message) { this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f); } +void DeltaSolBS2009Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "Deltasol BS 2009:"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "System Time", this->time_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolBS2009Sensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[12]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 10)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 18)); + if (this->heat_quantity_sensor_ != nullptr) { + this->heat_quantity_sensor_->publish_state(get_u16(message, 28) + get_u16(message, 30) * 1000); + } + if (this->time_sensor_ != nullptr) + this->time_sensor_->publish_state(get_u16(message, 22)); + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 32) * 0.01f); +} + void DeltaSolCSensor::dump_config() { ESP_LOGCONFIG(TAG, "Deltasol C:"); LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); @@ -166,9 +207,9 @@ void DeltaSolCSPlusSensor::handle_message(std::vector &message) { if (this->heat_quantity_sensor_ != nullptr) this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28)); if (this->time_sensor_ != nullptr) - this->time_sensor_->publish_state(get_u16(message, 12)); + this->time_sensor_->publish_state(get_u16(message, 22)); if (this->version_sensor_ != nullptr) - this->version_sensor_->publish_state(get_u16(message, 26) * 0.01f); + this->version_sensor_->publish_state(get_u16(message, 32) * 0.01f); if (this->flow_rate_sensor_ != nullptr) this->flow_rate_sensor_->publish_state(get_u16(message, 38)); } diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h index 6ba752b68c..d5535b2019 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.h +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -37,6 +37,37 @@ class DeltaSolBSPlusSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; +class DeltaSolBS2009Sensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *time_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + class DeltaSolCSensor : public VBusListener, public Component { public: void dump_config() override; 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/const.py b/esphome/const.py index 470f8a46e5..f07eb49b5a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.6.0-dev" +__version__ = "2023.7.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( 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/requirements_test.txt b/requirements_test.txt index c099250bd7..d5235d733b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,3 +11,5 @@ pytest-mock==3.10.0 pytest-asyncio==0.21.0 asyncmock==0.4.2 hypothesis==5.49.0 + +clang-format==13.0.1 ; platform_machine != 'armv7l' diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index dba6f47d43..5a0c92350d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -18,6 +18,7 @@ will be generated, they still need to be formatted """ import re +import os from pathlib import Path from textwrap import dedent from subprocess import call @@ -944,3 +945,19 @@ with open(root / "api_pb2_service.cpp", "w") as f: f.write(cpp) prot.unlink() + +try: + import clang_format + + def exec_clang_format(path): + clang_format_path = os.path.join( + os.path.dirname(clang_format.__file__), "data", "bin", "clang-format" + ) + call([clang_format_path, "-i", path]) + + exec_clang_format(root / "api_pb2_service.h") + exec_clang_format(root / "api_pb2_service.cpp") + exec_clang_format(root / "api_pb2.h") + exec_clang_format(root / "api_pb2.cpp") +except ImportError: + pass diff --git a/tests/test1.yaml b/tests/test1.yaml index c1aee5d536..cfa622dd4d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3417,3 +3417,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()));