diff --git a/esphome/components/status_indicator/__init__.py b/esphome/components/status_indicator/__init__.py new file mode 100644 index 0000000000..047d1f593d --- /dev/null +++ b/esphome/components/status_indicator/__init__.py @@ -0,0 +1,121 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +import esphome.automation as auto +from esphome.const import ( + CONF_ID, + CONF_PRIORITY, + CONF_GROUP, + CONF_TRIGGER_ID, +) + +from esphome.core import coroutine_with_priority + +CODEOWNERS = ["@nielsnl68"] + +status_led_ns = cg.esphome_ns.namespace("status_indicator") +StatusLED = status_led_ns.class_("StatusIndicator", cg.Component) +StatusTrigger = status_led_ns.class_("StatusTrigger", auto.Trigger.template()) +StatusAction = status_led_ns.class_("StatusAction", auto.Trigger.template()) +CONF_TRIGGER_LIST = { + "on_app_error": True, + "on_clear_app_error": True, + "on_app_warning": True, + "on_clear_app_warning": True, + "on_network_connected": True, + "on_network_disconnected": True, + "on_wifi_ap_enabled": True, + "on_wifi_ap_disabled": True, + "on_api_connected": True, + "on_api_disconnected": True, + "on_mqtt_connected": True, + "on_mgtt_disconnected": True, + "on_custom_status": False, +} + + +def trigger_setup(Single): + return auto.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StatusTrigger), + cv.Optional(CONF_GROUP, default=""): cv.string, + cv.Optional(CONF_PRIORITY, default=0): cv.int_range(0, 10), + }, + single=Single, + ) + + +def add_default_triggers(): + result = {} + + for trigger, single in CONF_TRIGGER_LIST.items(): + result[cv.Optional(trigger)] = trigger_setup(single) + return cv.Schema(result) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(StatusLED), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(add_default_triggers()) +) + + +async def add_trigger(var, conf, key): + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], + var, + conf[CONF_GROUP], + conf[CONF_PRIORITY], + ) + await auto.build_automation(trigger, [], conf) + cg.add(var.set_trigger(key, trigger)) + + +@coroutine_with_priority(80.0) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + for trigger_name, single in CONF_TRIGGER_LIST.items(): + conf = config.get(trigger_name, None) + if conf is None: + continue + if single: + await add_trigger(var, conf, trigger_name) + else: + for conf in config.get(trigger_name, []): + await add_trigger(var, conf, conf[CONF_TRIGGER_ID].id) + + +@auto.register_action( + "status.push", + StatusAction, + cv.Schema( + { + cv.Required(CONF_TRIGGER_ID): cv.use_id(StatusTrigger), + } + ), +) +async def status_action_push_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_TRIGGER_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + cg.add(var.set_state(True)) + return var + + +@auto.register_action( + "status.pop", + StatusAction, + cv.Schema( + { + cv.Required(CONF_TRIGGER_ID): cv.use_id(StatusTrigger), + } + ), +) +async def status_action_pop_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_TRIGGER_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + cg.add(var.set_state(False)) + return var diff --git a/esphome/components/status_indicator/status_indicator.cpp b/esphome/components/status_indicator/status_indicator.cpp new file mode 100644 index 0000000000..c2577dac24 --- /dev/null +++ b/esphome/components/status_indicator/status_indicator.cpp @@ -0,0 +1,153 @@ +#include "status_indicator.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#include "esphome/components/network/util.h" + +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + +#ifdef USE_MQTT +#include "esphome/components/mqtt/mqtt_client.h" +#endif +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif + +namespace esphome { +namespace status_indicator { + +static const char *const TAG = "status_indicator"; + +void StatusIndicator::dump_config() { ESP_LOGCONFIG(TAG, "Status Indicator:"); } +void StatusIndicator::loop() { + std::string status{""}; + if ((App.get_app_state() & STATUS_LED_ERROR) != 0u) { + status = "on_app_error"; + this->status_.on_error = 1; + } else if (this->status_.on_error == 1) { + status = "on_clear_app_error"; + this->status_.on_error = 0; + } else if ((App.get_app_state() & STATUS_LED_WARNING) != 0u) { + status = "on_app_warning"; + this->status_.on_warning = 1; + } else if (this->status_.on_warning == 1) { + status = "on_clear_app_warning"; + this->status_.on_warning = 0; + } + if (this->current_trigger_ != nullptr) { + if (this->current_trigger_->is_action_running()) { + if (status == "") { + return; + } + this->current_trigger_->stop_action(); + } + } + if (network::has_network()) { +#ifdef USE_WIFI + if (status == "" && wifi::global_wifi_component->is_ap_enabled()) { + status = "on_wifi_ap_enabled"; + this->status_.on_wifi_ap = 1; + } else if (this->status_.on_wifi_ap == 1) { + status = "on_wifi_ap_disabled"; + this->status_.on_wifi_ap = 0; + } +#endif + + if (status == "" && not network::is_connected()) { + status = "on_network_disconnected"; + this->status_.on_network = 1; + } else if (this->status_.on_network == 1) { + status = "on_network_connected"; + this->status_.on_network = 0; + } + +#ifdef USE_API + if (status == "" && api::global_api_server != nullptr && not api::global_api_server->is_connected()) { + status = "on_api_disconnected"; + this->status_.on_api = 1; + } else if (this->status_.on_error == 1) { + status = "on_api_connected"; + this->status_.on_api = 0; + } +#endif +#ifdef USE_MQTT + if (status == "" && mqtt::global_mqtt_client != nullptr && not mqtt::global_mqtt_client->is_connected()) { + status = "on_mqtt_disconnected"; + this->status_.on_mqtt = 1; + } else if (this->status_.on_mqtt == 1) { + status = "on_mqtt_connected"; + this->status_.on_mqtt = 0; + } +#endif + } + if (this->current_status_ != status) { + if (status != "") { + this->current_trigger_ = get_trigger(status); + if (this->current_trigger_ != nullptr) { + this->current_trigger_->trigger(); + } + } else { + this->current_trigger_ = nullptr; + if (!this->custom_triggers_.empty()) { + this->custom_triggers_[0]->trigger(); + } + } + this->current_status_ = status; + } +} + +float StatusIndicator::get_setup_priority() const { return setup_priority::HARDWARE; } +float StatusIndicator::get_loop_priority() const { return 50.0f; } + +StatusTrigger *StatusIndicator::get_trigger(std::string key) { + auto search = this->triggers_.find(key); + if (search != this->triggers_.end()) + return search->second; + else { + return nullptr; + } +} + +void StatusIndicator::set_trigger(std::string key, StatusTrigger *trigger) { this->triggers_[key] = trigger; } + +void StatusIndicator::push_trigger(StatusTrigger *trigger) { + this->pop_trigger(trigger, true); + uint32_t x = 0; + while (this->custom_triggers_.size() > x) { + if (trigger->get_priority() <= this->custom_triggers_[x]->get_priority()) { + this->custom_triggers_.insert(this->custom_triggers_.begin() + x, trigger); + break; + } else { + x++; + } + } +} + +void StatusIndicator::pop_trigger(StatusTrigger *trigger, bool incl_group) { + uint32_t x = 0; + while (this->custom_triggers_.size() > x) { + if (trigger == this->custom_triggers_[x]) { + this->custom_triggers_.erase(this->custom_triggers_.begin() + x); + } else if (incl_group && trigger->get_group() != "" && trigger->get_group() == this->custom_triggers_[x]->get_group()) { + this->custom_triggers_.erase(this->custom_triggers_.begin() + x); + } else { + x++; + } + } +} + +void StatusIndicator::pop_trigger(std::string group) { + uint32_t x = 0; + while (this->custom_triggers_.size() > x) { + if ( group == this->custom_triggers_[x]->get_group()) { + this->custom_triggers_.erase(this->custom_triggers_.begin() + x); + } else { + x++; + } + } +} + +} // namespace status_indicator +} // namespace esphome diff --git a/esphome/components/status_indicator/status_indicator.h b/esphome/components/status_indicator/status_indicator.h new file mode 100644 index 0000000000..402c01041d --- /dev/null +++ b/esphome/components/status_indicator/status_indicator.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/light/automation.h" + +namespace esphome { +namespace status_indicator { +class StatusTrigger; +union StatusFlags { + struct { + int on_error : 1; + int on_warning : 1; + int on_network : 1; + int on_api : 1; + int on_mqtt : 1; + int on_wifi_ap : 1; + }; + int setter = 0; +}; + +class StatusIndicator : public Component { + public: + void dump_config() override; + void loop() override; + + float get_setup_priority() const override; + float get_loop_priority() const override; + + StatusTrigger *get_trigger(std::string key); + void set_trigger(std::string key, StatusTrigger *trigger); + void push_trigger(StatusTrigger *trigger); + void pop_trigger(StatusTrigger *trigger, bool incl_group = false); + void pop_trigger(std::string group); + + protected: + std::string current_status_{""}; + StatusTrigger *current_trigger_{nullptr}; + StatusFlags status_; + std::map triggers_{}; + std::vector custom_triggers_{}; +}; + +class StatusTrigger : public Trigger<> { + public: + explicit StatusTrigger(StatusIndicator *parent, std::string group, uint32_t priority) + : parent_(parent), group_(group), priority_(priority) {} + std::string get_group() { return this->group_; } + uint32 get_priority() { return this->priority_; } + void push_me() { parent_->push_trigger(this); } + void pop_me() { parent_->pop_trigger(this, false); } + + protected: + StatusIndicator *parent_; + std::string group_; /// Minimum length of click. 0 means no minimum. + uint32_t priority_; /// Maximum length of click. 0 means no maximum. +}; + +template class StatusCondition : public Condition { + public: + StatusCondition(StatusIndicator *parent, bool state) : parent_(parent), state_(state) {} + bool check(Ts... x) override { return (this->parent_->status_.setter == 0) == this->state_; } + + protected: + StatusIndicator *parent_; + bool state_; +}; + +template class StatusAction : public Action { + public: + explicit StatusAction(StatusTrigger *trigger) : trigger_(trigger) {} + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + if (this->state_) { + + } else { + + } + } + + protected: + StatusTrigger *trigger_; +}; + +} // namespace status_indicator +} // namespace esphome