This commit is contained in:
NP v/d Spek 2024-05-02 15:25:24 +12:00 committed by GitHub
commit 1c0d0f6912
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 632 additions and 0 deletions

View File

@ -352,6 +352,7 @@ esphome/components/st7701s/* @clydebarrow
esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/status_indicator/* @nielsnl68
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931

View File

@ -0,0 +1,164 @@
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,
CONF_ON_TURN_OFF,
CONF_VALUE,
)
from esphome.schema_extractors import (
SCHEMA_EXTRACT,
schema_extractor,
)
from esphome.core import coroutine_with_priority
CODEOWNERS = ["@nielsnl68"]
status_led_ns = cg.esphome_ns.namespace("status_indicator")
StatusIndicator = status_led_ns.class_("StatusIndicator", cg.Component)
StatusTrigger = status_led_ns.class_("StatusTrigger", auto.Trigger.template())
StatusAction = status_led_ns.class_("StatusAction", auto.Action)
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,
}
CONF_WHICH = "which"
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(StatusIndicator),
cv.Required(CONF_ON_TURN_OFF): trigger_setup(True),
}
)
.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],
conf[CONF_TRIGGER_ID].id,
)
await auto.build_automation(trigger, [], conf)
if key is not None:
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)
await add_trigger(var, config.get(CONF_ON_TURN_OFF), CONF_ON_TURN_OFF)
for trigger_name, single in CONF_TRIGGER_LIST.items():
conf = config.get(trigger_name, None)
if conf is not None:
if single:
await add_trigger(var, conf, trigger_name)
else:
for conf in config.get(trigger_name, []):
await add_trigger(var, conf, None)
def maybe_simple_valuex(*validators, **kwargs):
key = kwargs.pop("key", CONF_VALUE)
validator = cv.All(*validators)
@schema_extractor("maybe")
def validate(value):
if value == SCHEMA_EXTRACT:
return (validator, key)
if isinstance(value, dict):
return validator(value)
return validator({key: value})
return validate
@auto.register_action(
"status.push",
StatusAction,
maybe_simple_valuex(
{
cv.GenerateID(CONF_ID): cv.use_id(StatusIndicator),
cv.Required(CONF_TRIGGER_ID): cv.use_id(StatusTrigger),
},
key=CONF_TRIGGER_ID,
),
)
async def status_action_push_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(True))
trigger = await cg.get_variable(config[CONF_TRIGGER_ID])
cg.add(var.set_trigger(trigger))
return var
@auto.register_action(
"status.pop",
StatusAction,
maybe_simple_valuex(
{
cv.GenerateID(CONF_ID): cv.use_id(StatusIndicator),
cv.Optional(CONF_GROUP): cv.string,
cv.Optional(CONF_TRIGGER_ID): cv.use_id(StatusTrigger),
},
cv.has_exactly_one_key(CONF_GROUP, CONF_TRIGGER_ID),
key=CONF_TRIGGER_ID,
),
)
async def status_action_pop_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(False))
if CONF_TRIGGER_ID in config:
trigger = await cg.get_variable(config[CONF_TRIGGER_ID])
cg.add(var.set_trigger(trigger))
else:
cg.add(var.set_group(config[CONF_GROUP]))
return var

View File

@ -0,0 +1,5 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
/secrets.yaml

View File

@ -0,0 +1,146 @@
---
esphome:
name: m5stack-atom-echo
esp32:
board: m5stack-atom
logger:
level: DEBUG
light:
- platform: esp32_rmt_led_strip
id: led
pin: GPIO27
default_transition_length: 0s
chipset: SK6812
num_leds: 1
rgb_order: grb
rmt_channel: 0
effects:
- pulse:
name: "Slow Pulse"
transition_length: 250ms
update_interval: 250ms
min_brightness: 50%
max_brightness: 100%
- pulse:
name: "Fast Pulse"
transition_length: 100ms
update_interval: 100ms
min_brightness: 50%
max_brightness: 100%
wifi:
ssid: LumenIOT24
password: Lum3nS0fT
binary_sensor:
- platform: gpio
pin:
number: GPIO39
inverted: true
name: Button
disabled_by_default: true
entity_category: diagnostic
id: button
on_press:
- status.push: custom_status2
- status.push: custom_status3
- status.push: custom_status1
on_release:
- status.pop:
group: "custom"
- delay: 5s
- status.pop: custom_status1
status_indicator:
on_turn_off: # Manditory
- light.turn_off:
id: led
on_app_error:
then:
- light.turn_on:
id: led
red: 100%
green: 0%
blue: 0%
brightness: 100%
effect: None
on_app_warning:
then:
- light.turn_on:
id: led
red: 100%
green: 0%
blue: 0%
brightness: 100%
effect: Slow Pulse
on_wifi_ap_enabled:
then:
- light.turn_on:
id: led
red: 100%
green: 100%
blue: 50%
brightness: 100%
effect: None
on_network_disconnected:
then:
- light.turn_on:
id: led
red: 100%
green: 0%
blue: 0%
brightness: 100%
effect: Fast Pulse
on_api_disconnected:
then:
- light.turn_on:
id: led
red: 100%
green: 100%
blue: 0%
brightness: 100%
effect: Fast Pulse
on_custom_status:
- trigger_id: custom_status1
priority: 1
then:
- light.turn_on:
id: led
red: 0%
green: 100%
blue: 0%
brightness: 100%
effect: None
- trigger_id: custom_status2
group: custom
priority: 3
then:
- light.turn_on:
id: led
red: 50%
green: 0%
blue: 100%
brightness: 100%
effect: None
- trigger_id: custom_status3
group: custom1
priority: 1
then:
- light.turn_on:
id: led
red: 100%
green: 100%
blue: 50%
brightness: 100%
effect: Fast pulse
- delay: 10s
- status.pop: custom_status3

View File

@ -0,0 +1,213 @@
#include "status_indicator.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#ifdef USE_ETHERNET
#include "esphome/components/ethernet/ethernet_component.h"
#endif
#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 {
bool has_network() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr)
return true;
#endif
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr)
return true;
#endif
#ifdef USE_HOST
return true; // Assume its connected
#endif
return false;
}
bool is_connected() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected())
return true;
#endif
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->is_connected();
#endif
#ifdef USE_HOST
return true; // Assume its connected
#endif
return false;
}
static const char *const TAG = "status_indicator";
void StatusIndicator::dump_config() {
ESP_LOGCONFIG(TAG, "Status Indicator supports:");
for (auto i = this->triggers_.begin(); i != this->triggers_.end(); i++) {
ESP_LOGCONFIG(TAG, " * %s: %s", i->first.c_str(), i->second->get_info().c_str());
}
}
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) {
status = "on_clear_app_error";
this->status_.on_error = 0;
}
if (has_network()) {
#ifdef USE_WIFI
if (status.empty() && 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) {
status = "on_wifi_ap_disabled";
this->status_.on_wifi_ap = 0;
}
#endif
if (status.empty() && not is_connected()) {
status = "on_network_disconnected";
this->status_.on_network = 1;
} else if (this->status_.on_network) {
status = "on_network_connected";
this->status_.on_network = 0;
}
#ifdef USE_API
if (status.empty() && 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_api) {
status = "on_api_connected";
this->status_.on_api = 0;
}
#endif
#ifdef USE_MQTT
if (status.empty() && 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) {
status = "on_mqtt_connected";
this->status_.on_mqtt = 0;
}
#endif
}
if (status.empty() && (App.get_app_state() & STATUS_LED_WARNING) != 0u) {
status = "on_app_warning";
this->status_.on_warning = 1;
} else if (this->status_.on_warning) {
status = "on_clear_app_warning";
this->status_.on_warning = 0;
}
if (this->current_status_ != status) {
if (this->current_trigger_ != nullptr and this->current_trigger_->is_action_running() and !status.empty()) {
this->current_trigger_->stop_action();
}
StatusTrigger *oldtrigger = this->current_trigger_;
if (!status.empty() && this->triggers_.count(status) == 1) {
this->current_trigger_ = get_trigger(status);
} else if (!this->stack_.empty()) {
this->current_trigger_ = this->stack_.back();
status = "on_custom_status";
} else {
this->current_trigger_ = get_trigger("on_turn_off");
status = "on_turn_off";
}
if (oldtrigger != this->current_trigger_) {
ESP_LOGI(TAG, "<>> %s->%s", status.c_str(), this->current_trigger_->get_name().c_str());
this->current_trigger_->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(const std::string &key) {
auto search = this->triggers_.find(key);
if (search != this->triggers_.end()) {
return search->second;
} else {
return nullptr;
}
}
void StatusIndicator::set_trigger(const std::string &key, StatusTrigger *trigger) { this->triggers_[key] = trigger; }
void StatusIndicator::push_trigger(StatusTrigger *trigger) {
this->pop_trigger(trigger, true);
ESP_LOGD(TAG, "Push ID: %s", trigger->get_info().c_str());
for (auto i = this->stack_.begin(); i != this->stack_.end(); ++i) {
StatusTrigger *st = *i;
if (trigger->get_priority() < st->get_priority()) {
this->stack_.insert(i, trigger);
this->current_status_ = "update me";
// log_triggers_();
return;
}
}
this->stack_.push_back(trigger);
this->current_status_ = "update me";
// log_triggers_();
}
void StatusIndicator::pop_trigger(StatusTrigger *trigger, bool incl_group) {
incl_group = incl_group && !trigger->get_group().empty();
ESP_LOGD(TAG, "Pop by ID: %s || %s", trigger->get_info().c_str(), YESNO(incl_group));
std::string group = trigger->get_group();
for (auto i = this->stack_.begin(); i != this->stack_.end();) {
StatusTrigger *st = *i;
if ((incl_group && group == st->get_group()) || (trigger == st)) {
this->stack_.erase(i);
this->current_status_ = "update me";
} else {
++i;
}
}
// log_triggers_();
}
void StatusIndicator::pop_trigger(const std::string &group) {
ESP_LOGD(TAG, "Pop by group: %s", group.c_str());
for (auto i = this->stack_.begin(); i != this->stack_.end();) {
StatusTrigger *st = *i;
if (group == st->get_group()) {
this->stack_.erase(i);
this->current_status_ = "update me";
} else {
++i;
}
}
// log_triggers_();
}
void StatusIndicator::log_triggers_() {
for (auto *st : this->stack_) {
ESP_LOGD(TAG, "%s", st->get_info().c_str());
}
ESP_LOGD(TAG, "----------------------------- %d ----", this->stack_.size());
}
} // namespace status_indicator
} // namespace esphome

View File

@ -0,0 +1,100 @@
#pragma once
#include <map>
#include <vector>
#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(const std::string &key);
void set_trigger(const std::string &key, StatusTrigger *trigger);
void push_trigger(StatusTrigger *trigger);
void pop_trigger(StatusTrigger *trigger, bool incl_group = false);
void pop_trigger(const std::string &group);
protected:
void log_triggers_();
std::string current_status_{""};
StatusTrigger *current_trigger_{nullptr};
StatusFlags status_;
std::map<std::string, StatusTrigger *> triggers_{};
std::vector<StatusTrigger *> stack_{};
};
class StatusTrigger : public Trigger<> {
public:
explicit StatusTrigger(StatusIndicator *parent, std::string group, uint32_t priority, std::string name)
: parent_(parent), name_(std::move(name)), group_(std::move(group)), priority_(priority) {}
std::string get_group() { return this->group_; }
uint32_t get_priority() { return this->priority_; }
std::string get_name() { return this->name_; }
std::string get_info() { return this->name_ + " \t| " + this->group_ + " \t| " + std::to_string(this->priority_); }
protected:
StatusIndicator *parent_;
std::string name_; /// Minimum length of click. 0 means no minimum.
std::string group_; /// Minimum length of click. 0 means no minimum.
uint32_t priority_; /// Maximum length of click. 0 means no maximum.
};
template<typename... Ts> class StatusCondition : public Condition<Ts...> {
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<typename... Ts> class StatusAction : public Action<Ts...>, public Parented<StatusIndicator> {
public:
void set_state(bool state) { this->state_ = state; }
void set_trigger(StatusTrigger *trigger) { this->trigger_ = trigger; }
void set_group(const std::string &group) { this->group_ = group; }
void play(Ts... x) override {
if (this->state_) {
if (this->trigger_ != nullptr) {
this->parent_->push_trigger(this->trigger_);
}
} else if (this->group_ != "") {
this->parent_->pop_trigger(this->group_);
} else if (this->trigger_ != nullptr) {
this->parent_->pop_trigger(this->trigger_, false);
}
}
protected:
StatusIndicator *indicator_;
StatusTrigger *trigger_{nullptr};
std::string group_{""};
bool state_{false};
};
} // namespace status_indicator
} // namespace esphome

View File

@ -201,6 +201,8 @@ void WiFiComponent::loop() {
WiFiComponent::WiFiComponent() { global_wifi_component = this; }
bool WiFiComponent::has_ap() const { return this->has_ap_; }
bool WiFiComponent::is_ap_enabled() const { return this->ap_setup_; }
bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; }
#ifdef USE_WIFI_11KV_SUPPORT

View File

@ -251,6 +251,7 @@ class WiFiComponent : public Component {
bool has_sta() const;
bool has_ap() const;
bool is_ap_enabled() const;
#ifdef USE_WIFI_11KV_SUPPORT
void set_btm(bool btm);