mirror of
https://github.com/esphome/esphome.git
synced 2026-01-28 15:32:07 -07:00
Compare commits
18 Commits
chunked_fi
...
copilot/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
393f1007e3 | ||
|
|
d86048cc2d | ||
|
|
e1355de4cb | ||
|
|
7385c4cf3d | ||
|
|
3bd6ec4ec7 | ||
|
|
051604f284 | ||
|
|
10dfd95ff2 | ||
|
|
4696d7eb6a | ||
|
|
bc2e8e33ee | ||
|
|
4c0e0b8d76 | ||
|
|
48b80858a7 | ||
|
|
081081f69a | ||
|
|
8abb783b64 | ||
|
|
bccfe9eead | ||
|
|
35fb44da36 | ||
|
|
1dfb8926d3 | ||
|
|
0d63c755b7 | ||
|
|
6f2ca4c2a7 |
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition, maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
@@ -9,6 +11,7 @@ from esphome.const import (
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_IDLE,
|
||||
CONF_ON_OPEN,
|
||||
CONF_POSITION,
|
||||
CONF_POSITION_COMMAND_TOPIC,
|
||||
@@ -53,6 +56,8 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_WINDOW,
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
cover_ns = cg.esphome_ns.namespace("cover")
|
||||
|
||||
Cover = cover_ns.class_("Cover", cg.EntityBase)
|
||||
@@ -83,14 +88,30 @@ ControlAction = cover_ns.class_("ControlAction", automation.Action)
|
||||
CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action)
|
||||
CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition)
|
||||
CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition)
|
||||
|
||||
# Triggers
|
||||
CoverOpenTrigger = cover_ns.class_("CoverOpenTrigger", automation.Trigger.template())
|
||||
CoverOpenedTrigger = cover_ns.class_(
|
||||
"CoverOpenedTrigger", automation.Trigger.template()
|
||||
)
|
||||
CoverClosedTrigger = cover_ns.class_(
|
||||
"CoverClosedTrigger", automation.Trigger.template()
|
||||
)
|
||||
CoverTrigger = cover_ns.class_("CoverTrigger", automation.Trigger.template())
|
||||
|
||||
# Cover-specific constants
|
||||
CONF_ON_CLOSED = "on_closed"
|
||||
CONF_ON_OPENED = "on_opened"
|
||||
CONF_ON_OPENING = "on_opening"
|
||||
CONF_ON_CLOSING = "on_closing"
|
||||
|
||||
OPERATIONS = (
|
||||
CONF_ON_CLOSING,
|
||||
CONF_ON_OPENING,
|
||||
CONF_ON_IDLE,
|
||||
)
|
||||
|
||||
|
||||
def get_operation_from_conf_(conf: str) -> CoverOperation:
|
||||
return getattr(CoverOperation, "COVER_OPERATION_" + conf.split("_")[1].upper())
|
||||
|
||||
|
||||
_COVER_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
@@ -111,9 +132,15 @@ _COVER_SCHEMA = (
|
||||
cv.Optional(CONF_TILT_STATE_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||
),
|
||||
# Deprecated trigger
|
||||
cv.Optional(CONF_ON_OPEN): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger),
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_OPENED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLOSED): automation.validate_automation(
|
||||
@@ -121,6 +148,16 @@ _COVER_SCHEMA = (
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger),
|
||||
}
|
||||
),
|
||||
**{
|
||||
cv.Optional(conf): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
CoverTrigger.template(get_operation_from_conf_(conf))
|
||||
),
|
||||
}
|
||||
)
|
||||
for conf in OPERATIONS
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -157,12 +194,23 @@ async def setup_cover_core_(var, config):
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
|
||||
for conf in config.get(CONF_ON_OPEN, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_CLOSED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
if on_opens := config.get(CONF_ON_OPEN):
|
||||
_LOGGER.warning(
|
||||
"The 'on_open' trigger for covers is deprecated and will be removed in a future release. Please use 'on_opened' instead."
|
||||
)
|
||||
for conf in on_opens:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for on_op in OPERATIONS:
|
||||
if triggers := config.get(on_op):
|
||||
for conf in triggers:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for on_state in [CONF_ON_OPENED, CONF_ON_CLOSED]:
|
||||
if triggers := config.get(on_state):
|
||||
for conf in triggers:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
@@ -258,6 +306,26 @@ async def cover_control_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"cover.is_open",
|
||||
CoverIsOpenCondition,
|
||||
cv.maybe_simple_value({cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID),
|
||||
)
|
||||
async def cover_is_open_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)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"cover.is_closed",
|
||||
CoverIsClosedCondition,
|
||||
cv.maybe_simple_value({cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID),
|
||||
)
|
||||
async def cover_is_closed_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(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_global(cover_ns.using)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "cover.h"
|
||||
|
||||
namespace esphome::cover {
|
||||
|
||||
template<typename... Ts> class OpenAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit OpenAction(Cover *cover) : cover_(cover) {}
|
||||
@@ -72,6 +71,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
|
||||
template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
|
||||
public:
|
||||
CoverPublishAction(Cover *cover) : cover_(cover) {}
|
||||
|
||||
TEMPLATABLE_VALUE(float, position)
|
||||
TEMPLATABLE_VALUE(float, tilt)
|
||||
TEMPLATABLE_VALUE(CoverOperation, current_operation)
|
||||
@@ -93,7 +93,8 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
|
||||
template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
|
||||
public:
|
||||
CoverIsOpenCondition(Cover *cover) : cover_(cover) {}
|
||||
bool check(const Ts &...x) override { return this->cover_->is_fully_open(); }
|
||||
|
||||
bool check(const Ts &...x) override { return this->cover_->position == COVER_OPEN; }
|
||||
|
||||
protected:
|
||||
Cover *cover_;
|
||||
@@ -102,32 +103,60 @@ template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
|
||||
template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> {
|
||||
public:
|
||||
CoverIsClosedCondition(Cover *cover) : cover_(cover) {}
|
||||
bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); }
|
||||
|
||||
bool check(const Ts &...x) override { return this->cover_->position == COVER_CLOSED; }
|
||||
|
||||
protected:
|
||||
Cover *cover_;
|
||||
};
|
||||
|
||||
class CoverOpenTrigger : public Trigger<> {
|
||||
class CoverOpenedTrigger : public Trigger<> {
|
||||
public:
|
||||
CoverOpenTrigger(Cover *a_cover) {
|
||||
CoverOpenedTrigger(Cover *a_cover) {
|
||||
a_cover->add_on_state_callback([this, a_cover]() {
|
||||
if (a_cover->is_fully_open()) {
|
||||
this->trigger();
|
||||
if (a_cover->position != this->last_position_) {
|
||||
this->last_position_ = a_cover->position;
|
||||
if (a_cover->position == COVER_OPEN)
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
float last_position_{NAN};
|
||||
};
|
||||
|
||||
class CoverClosedTrigger : public Trigger<> {
|
||||
public:
|
||||
CoverClosedTrigger(Cover *a_cover) {
|
||||
a_cover->add_on_state_callback([this, a_cover]() {
|
||||
if (a_cover->is_fully_closed()) {
|
||||
this->trigger();
|
||||
if (a_cover->position != this->last_position_) {
|
||||
this->last_position_ = a_cover->position;
|
||||
if (a_cover->position == COVER_CLOSED)
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
float last_position_{NAN};
|
||||
};
|
||||
|
||||
template<CoverOperation OP> class CoverTrigger : public Trigger<> {
|
||||
public:
|
||||
CoverTrigger(Cover *a_cover) {
|
||||
a_cover->add_on_state_callback([this, a_cover]() {
|
||||
auto current_op = a_cover->current_operation;
|
||||
if (current_op == OP) {
|
||||
if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) {
|
||||
this->trigger();
|
||||
}
|
||||
}
|
||||
this->last_operation_ = current_op;
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
optional<CoverOperation> last_operation_{};
|
||||
};
|
||||
} // namespace esphome::cover
|
||||
|
||||
@@ -10,9 +10,6 @@ namespace esphome::cover {
|
||||
|
||||
static const char *const TAG = "cover";
|
||||
|
||||
const float COVER_OPEN = 1.0f;
|
||||
const float COVER_CLOSED = 0.0f;
|
||||
|
||||
const LogString *cover_command_to_str(float pos) {
|
||||
if (pos == COVER_OPEN) {
|
||||
return LOG_STR("OPEN");
|
||||
@@ -153,23 +150,23 @@ void Cover::publish_state(bool save) {
|
||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
|
||||
|
||||
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
|
||||
ESP_LOGV(TAG, "'%s' >>", this->name_.c_str());
|
||||
auto traits = this->get_traits();
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);
|
||||
ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f);
|
||||
} else {
|
||||
if (this->position == COVER_OPEN) {
|
||||
ESP_LOGD(TAG, " State: OPEN");
|
||||
ESP_LOGV(TAG, " State: OPEN");
|
||||
} else if (this->position == COVER_CLOSED) {
|
||||
ESP_LOGD(TAG, " State: CLOSED");
|
||||
ESP_LOGV(TAG, " State: CLOSED");
|
||||
} else {
|
||||
ESP_LOGD(TAG, " State: UNKNOWN");
|
||||
ESP_LOGV(TAG, " State: UNKNOWN");
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_tilt()) {
|
||||
ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f);
|
||||
ESP_LOGV(TAG, " Tilt: %.0f%%", this->tilt * 100.0f);
|
||||
}
|
||||
ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation)));
|
||||
ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation)));
|
||||
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY)
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
namespace esphome::cover {
|
||||
|
||||
const extern float COVER_OPEN;
|
||||
const extern float COVER_CLOSED;
|
||||
static constexpr const float COVER_OPEN = 1.0f;
|
||||
static constexpr const float COVER_CLOSED = 0.0f;
|
||||
|
||||
#define LOG_COVER(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
|
||||
@@ -175,6 +175,32 @@ ESP32_BOARD_PINS = {
|
||||
"LED": 13,
|
||||
"LED_BUILTIN": 13,
|
||||
},
|
||||
"adafruit_feather_esp32s3_reversetft": {
|
||||
"BUTTON": 0,
|
||||
"A0": 18,
|
||||
"A1": 17,
|
||||
"A2": 16,
|
||||
"A3": 15,
|
||||
"A4": 14,
|
||||
"A5": 8,
|
||||
"SCK": 36,
|
||||
"MOSI": 35,
|
||||
"MISO": 37,
|
||||
"RX": 38,
|
||||
"TX": 39,
|
||||
"SCL": 4,
|
||||
"SDA": 3,
|
||||
"NEOPIXEL": 33,
|
||||
"PIN_NEOPIXEL": 33,
|
||||
"NEOPIXEL_POWER": 21,
|
||||
"TFT_I2C_POWER": 7,
|
||||
"TFT_CS": 42,
|
||||
"TFT_DC": 40,
|
||||
"TFT_RESET": 41,
|
||||
"TFT_BACKLIGHT": 45,
|
||||
"LED": 13,
|
||||
"LED_BUILTIN": 13,
|
||||
},
|
||||
"adafruit_feather_esp32s3_tft": {
|
||||
"BUTTON": 0,
|
||||
"A0": 18,
|
||||
|
||||
@@ -451,7 +451,7 @@ void LD2450Component::handle_periodic_data_() {
|
||||
int16_t ty = 0;
|
||||
int16_t td = 0;
|
||||
int16_t ts = 0;
|
||||
int16_t angle = 0;
|
||||
float angle = 0;
|
||||
uint8_t index = 0;
|
||||
Direction direction{DIRECTION_UNDEFINED};
|
||||
bool is_moving = false;
|
||||
|
||||
@@ -143,6 +143,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
],
|
||||
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
|
||||
unit_of_measurement=UNIT_DEGREES,
|
||||
accuracy_decimals=1,
|
||||
),
|
||||
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
|
||||
@@ -27,46 +27,61 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t
|
||||
}
|
||||
|
||||
void RuntimeStatsCollector::log_stats_() {
|
||||
// First pass: count active components
|
||||
size_t count = 0;
|
||||
for (const auto &it : this->component_stats_) {
|
||||
if (it.second.get_period_count() > 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"Component Runtime Statistics\n"
|
||||
" Period stats (last %" PRIu32 "ms):",
|
||||
this->log_interval_);
|
||||
" Period stats (last %" PRIu32 "ms): %zu active components",
|
||||
this->log_interval_, count);
|
||||
|
||||
// First collect stats we want to display
|
||||
std::vector<ComponentStatPair> stats_to_display;
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stack buffer sized to actual active count (up to 256 components), heap fallback for larger
|
||||
SmallBufferWithHeapFallback<256, Component *> buffer(count);
|
||||
Component **sorted = buffer.get();
|
||||
|
||||
// Second pass: fill buffer with active components
|
||||
size_t idx = 0;
|
||||
for (const auto &it : this->component_stats_) {
|
||||
Component *component = it.first;
|
||||
const ComponentRuntimeStats &stats = it.second;
|
||||
if (stats.get_period_count() > 0) {
|
||||
ComponentStatPair pair = {component, &stats};
|
||||
stats_to_display.push_back(pair);
|
||||
if (it.second.get_period_count() > 0) {
|
||||
sorted[idx++] = it.first;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
|
||||
std::sort(sorted, sorted + count, [this](Component *a, Component *b) {
|
||||
return this->component_stats_[a].get_period_time_ms() > this->component_stats_[b].get_period_time_ms();
|
||||
});
|
||||
|
||||
// Log top components by period runtime
|
||||
for (const auto &it : stats_to_display) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = this->component_stats_[sorted[i]];
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms",
|
||||
LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_period_count(),
|
||||
it.stats->get_period_avg_time_ms(), it.stats->get_period_max_time_ms(), it.stats->get_period_time_ms());
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), stats.get_period_avg_time_ms(),
|
||||
stats.get_period_max_time_ms(), stats.get_period_time_ms());
|
||||
}
|
||||
|
||||
// Log total stats since boot
|
||||
ESP_LOGI(TAG, " Total stats (since boot):");
|
||||
// Log total stats since boot (only for active components - idle ones haven't changed)
|
||||
ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
|
||||
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(stats_to_display.begin(), stats_to_display.end(),
|
||||
[](const ComponentStatPair &a, const ComponentStatPair &b) {
|
||||
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
|
||||
});
|
||||
std::sort(sorted, sorted + count, [this](Component *a, Component *b) {
|
||||
return this->component_stats_[a].get_total_time_ms() > this->component_stats_[b].get_total_time_ms();
|
||||
});
|
||||
|
||||
for (const auto &it : stats_to_display) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = this->component_stats_[sorted[i]];
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms",
|
||||
LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_total_count(),
|
||||
it.stats->get_total_avg_time_ms(), it.stats->get_total_max_time_ms(), it.stats->get_total_time_ms());
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), stats.get_total_avg_time_ms(),
|
||||
stats.get_total_max_time_ms(), stats.get_total_time_ms());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -77,17 +76,6 @@ class ComponentRuntimeStats {
|
||||
uint32_t total_max_time_ms_;
|
||||
};
|
||||
|
||||
// For sorting components by run time
|
||||
struct ComponentStatPair {
|
||||
Component *component;
|
||||
const ComponentRuntimeStats *stats;
|
||||
|
||||
bool operator>(const ComponentStatPair &other) const {
|
||||
// Sort by period time as that's what we're displaying in the logs
|
||||
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
|
||||
}
|
||||
};
|
||||
|
||||
class RuntimeStatsCollector {
|
||||
public:
|
||||
RuntimeStatsCollector();
|
||||
|
||||
@@ -27,6 +27,9 @@ void RealTimeClock::dump_config() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
|
||||
#endif
|
||||
auto time = this->now();
|
||||
ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour,
|
||||
time.minute, time.second);
|
||||
}
|
||||
|
||||
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
#include "esphome/components/esp32_improv/esp32_improv_component.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_IMPROV_SERIAL
|
||||
#include "esphome/components/improv_serial/improv_serial_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::wifi {
|
||||
|
||||
static const char *const TAG = "wifi";
|
||||
@@ -365,6 +369,75 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WiFiComponent::needs_full_scan_results_() const {
|
||||
// Components that require full scan results (for example, scan result listeners)
|
||||
// are expected to call request_wifi_scan_results(), which sets keep_scan_results_.
|
||||
if (this->keep_scan_results_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
// Captive portal needs full results when active (showing network list to user)
|
||||
if (captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IMPROV_SERIAL
|
||||
// Improv serial needs results during provisioning (before connected)
|
||||
if (improv_serial::global_improv_serial_component != nullptr && !this->is_connected()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IMPROV
|
||||
// BLE improv also needs results during provisioning
|
||||
if (esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t *bssid) const {
|
||||
// Hidden networks in scan results have empty SSIDs - skip them
|
||||
if (ssid[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
for (const auto &sta : this->sta_) {
|
||||
// Skip hidden network configs (they don't appear in normal scans)
|
||||
if (sta.get_hidden()) {
|
||||
continue;
|
||||
}
|
||||
// For BSSID-only configs (empty SSID), match by BSSID
|
||||
if (sta.get_ssid().empty()) {
|
||||
if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Match by SSID
|
||||
if (sta.get_ssid() == ssid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
// Skip logging during roaming scans to avoid log buffer overflow
|
||||
// (roaming scans typically find many networks but only care about same-SSID APs)
|
||||
if (this->roaming_state_ == RoamingState::SCANNING) {
|
||||
return;
|
||||
}
|
||||
char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(bssid, bssid_s);
|
||||
ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " %ddB Ch:%u", ssid, bssid_s, rssi, channel);
|
||||
#endif
|
||||
}
|
||||
|
||||
int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
|
||||
// Find next SSID to try in RETRY_HIDDEN phase.
|
||||
//
|
||||
@@ -656,8 +729,12 @@ void WiFiComponent::loop() {
|
||||
ESP_LOGI(TAG, "Starting fallback AP");
|
||||
this->setup_ap_config_();
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (captive_portal::global_captive_portal != nullptr)
|
||||
if (captive_portal::global_captive_portal != nullptr) {
|
||||
// Reset so we force one full scan after captive portal starts
|
||||
// (previous scans were filtered because captive portal wasn't active yet)
|
||||
this->has_completed_scan_after_captive_portal_start_ = false;
|
||||
captive_portal::global_captive_portal->start();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1195,7 +1272,7 @@ template<typename VectorType> static void insertion_sort_scan_results(VectorType
|
||||
// has overhead from UART transmission, so combining INFO+DEBUG into one line halves
|
||||
// the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls.
|
||||
__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
|
||||
char bssid_s[18];
|
||||
char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
auto bssid = res.get_bssid();
|
||||
format_mac_addr_upper(bssid.data(), bssid_s);
|
||||
|
||||
@@ -1211,18 +1288,6 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
// Helper function to log non-matching scan results at verbose level
|
||||
__attribute__((noinline)) static void log_scan_result_non_matching(const WiFiScanResult &res) {
|
||||
char bssid_s[18];
|
||||
auto bssid = res.get_bssid();
|
||||
format_mac_addr_upper(bssid.data(), bssid_s);
|
||||
|
||||
ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
|
||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||
}
|
||||
#endif
|
||||
|
||||
void WiFiComponent::check_scanning_finished() {
|
||||
if (!this->scan_done_) {
|
||||
if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
|
||||
@@ -1232,6 +1297,8 @@ void WiFiComponent::check_scanning_finished() {
|
||||
return;
|
||||
}
|
||||
this->scan_done_ = false;
|
||||
this->has_completed_scan_after_captive_portal_start_ =
|
||||
true; // Track that we've done a scan since captive portal started
|
||||
this->retry_hidden_mode_ = RetryHiddenMode::SCAN_BASED;
|
||||
|
||||
if (this->scan_result_.empty()) {
|
||||
@@ -1259,21 +1326,12 @@ void WiFiComponent::check_scanning_finished() {
|
||||
// Sort scan results using insertion sort for better memory efficiency
|
||||
insertion_sort_scan_results(this->scan_result_);
|
||||
|
||||
size_t non_matching_count = 0;
|
||||
// Log matching networks (non-matching already logged at VERBOSE in scan callback)
|
||||
for (auto &res : this->scan_result_) {
|
||||
if (res.get_matches()) {
|
||||
log_scan_result(res);
|
||||
} else {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
log_scan_result_non_matching(res);
|
||||
#else
|
||||
non_matching_count++;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (non_matching_count > 0) {
|
||||
ESP_LOGD(TAG, "- %zu non-matching (VERBOSE to show)", non_matching_count);
|
||||
}
|
||||
|
||||
// SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
|
||||
// After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
|
||||
@@ -1532,7 +1590,10 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
||||
if (this->went_through_explicit_hidden_phase_()) {
|
||||
return WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||
}
|
||||
// Skip scanning when captive portal/improv is active to avoid disrupting AP.
|
||||
// Skip scanning when captive portal/improv is active to avoid disrupting AP,
|
||||
// BUT only if we've already completed at least one scan AFTER the portal started.
|
||||
// When captive portal first starts, scan results may be filtered/stale, so we need
|
||||
// to do one full scan to populate available networks for the captive portal UI.
|
||||
//
|
||||
// WHY SCANNING DISRUPTS AP MODE:
|
||||
// WiFi scanning requires the radio to leave the AP's channel and hop through
|
||||
@@ -1549,7 +1610,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
||||
//
|
||||
// This allows users to configure WiFi via captive portal while the device keeps
|
||||
// attempting to connect to all configured networks in sequence.
|
||||
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
|
||||
// Captive portal needs scan results to show available networks.
|
||||
// If captive portal is active, only skip scanning if we've done a scan after it started.
|
||||
// If only improv is active (no captive portal), skip scanning since improv doesn't need results.
|
||||
if (this->is_captive_portal_active_()) {
|
||||
if (this->has_completed_scan_after_captive_portal_start_) {
|
||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||
}
|
||||
// Need to scan for captive portal
|
||||
} else if (this->is_esp32_improv_active_()) {
|
||||
// Improv doesn't need scan results
|
||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||
}
|
||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||
@@ -2096,7 +2166,7 @@ void WiFiComponent::clear_roaming_state_() {
|
||||
|
||||
void WiFiComponent::release_scan_results_() {
|
||||
if (!this->keep_scan_results_) {
|
||||
#ifdef USE_RP2040
|
||||
#if defined(USE_RP2040) || defined(USE_ESP32)
|
||||
// std::vector - use swap trick since shrink_to_fit is non-binding
|
||||
decltype(this->scan_result_)().swap(this->scan_result_);
|
||||
#else
|
||||
|
||||
@@ -161,9 +161,12 @@ struct EAPAuth {
|
||||
|
||||
using bssid_t = std::array<uint8_t, 6>;
|
||||
|
||||
// Use std::vector for RP2040 since scan count is unknown (callback-based)
|
||||
// Use FixedVector for other platforms where count is queried first
|
||||
#ifdef USE_RP2040
|
||||
/// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID)
|
||||
static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8;
|
||||
|
||||
// Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API)
|
||||
// Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible
|
||||
#if defined(USE_RP2040) || defined(USE_ESP32)
|
||||
template<typename T> using wifi_scan_vector_t = std::vector<T>;
|
||||
#else
|
||||
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||
@@ -539,6 +542,13 @@ class WiFiComponent : public Component {
|
||||
/// Check if an SSID was seen in the most recent scan results
|
||||
/// Used to skip hidden mode for SSIDs we know are visible
|
||||
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
|
||||
/// Check if full scan results are needed (captive portal active, improv, listeners)
|
||||
bool needs_full_scan_results_() const;
|
||||
/// Check if network matches any configured network (for scan result filtering)
|
||||
/// Matches by SSID when configured, or by BSSID for BSSID-only configs
|
||||
bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const;
|
||||
/// Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow)
|
||||
void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel);
|
||||
/// Find next SSID that wasn't in scan results (might be hidden)
|
||||
/// Returns index of next potentially hidden SSID, or -1 if none found
|
||||
/// @param start_index Start searching from index after this (-1 to start from beginning)
|
||||
@@ -710,6 +720,8 @@ class WiFiComponent : public Component {
|
||||
bool enable_on_boot_{true};
|
||||
bool got_ipv4_address_{false};
|
||||
bool keep_scan_results_{false};
|
||||
bool has_completed_scan_after_captive_portal_start_{
|
||||
false}; // Tracks if we've completed a scan after captive portal started
|
||||
RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY};
|
||||
bool skip_cooldown_next_cycle_{false};
|
||||
bool post_connect_roaming_{true}; // Enabled by default
|
||||
|
||||
@@ -760,20 +760,35 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Count the number of results first
|
||||
auto *head = reinterpret_cast<bss_info *>(arg);
|
||||
bool needs_full = this->needs_full_scan_results_();
|
||||
|
||||
// First pass: count matching networks (linked list is non-destructive)
|
||||
size_t total = 0;
|
||||
size_t count = 0;
|
||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||
count++;
|
||||
total++;
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
this->scan_result_.init(count);
|
||||
this->scan_result_.init(count); // Exact allocation
|
||||
|
||||
// Second pass: store matching networks
|
||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||
this->scan_result_.emplace_back(
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN,
|
||||
it->is_hidden != 0);
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
|
||||
this->scan_result_.emplace_back(
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
this->scan_done_ = true;
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : global_wifi_component->scan_results_listeners_) {
|
||||
|
||||
@@ -828,11 +828,21 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
|
||||
uint16_t number = it.number;
|
||||
scan_result_.init(number);
|
||||
bool needs_full = this->needs_full_scan_results_();
|
||||
|
||||
// Smart reserve: full capacity if needed, small reserve otherwise
|
||||
if (needs_full) {
|
||||
this->scan_result_.reserve(number);
|
||||
} else {
|
||||
this->scan_result_.reserve(WIFI_SCAN_RESULT_FILTERED_RESERVE);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_HOSTED
|
||||
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
|
||||
// Presumably an upstream bug, work-around by getting all records at once
|
||||
auto records = std::make_unique<wifi_ap_record_t[]>(number);
|
||||
// Use stack buffer (3904 bytes / ~80 bytes per record = ~48 records) with heap fallback
|
||||
static constexpr size_t SCAN_RECORD_STACK_COUNT = 3904 / sizeof(wifi_ap_record_t);
|
||||
SmallBufferWithHeapFallback<SCAN_RECORD_STACK_COUNT, wifi_ap_record_t> records(number);
|
||||
err = esp_wifi_scan_get_ap_records(&number, records.get());
|
||||
if (err != ESP_OK) {
|
||||
esp_wifi_clear_ap_list();
|
||||
@@ -840,7 +850,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
return;
|
||||
}
|
||||
for (uint16_t i = 0; i < number; i++) {
|
||||
wifi_ap_record_t &record = records[i];
|
||||
wifi_ap_record_t &record = records.get()[i];
|
||||
#else
|
||||
// Process one record at a time to avoid large buffer allocation
|
||||
for (uint16_t i = 0; i < number; i++) {
|
||||
@@ -852,12 +862,23 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
break;
|
||||
}
|
||||
#endif // USE_ESP32_HOSTED
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
std::string ssid(reinterpret_cast<const char *>(record.ssid));
|
||||
scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN,
|
||||
ssid.empty());
|
||||
|
||||
// Check C string first - avoid std::string construction for non-matching networks
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(record.ssid);
|
||||
|
||||
// Only construct std::string and store if needed
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
std::string ssid(ssid_cstr);
|
||||
this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi,
|
||||
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
|
||||
@@ -670,18 +670,39 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
if (num < 0)
|
||||
return;
|
||||
|
||||
this->scan_result_.init(static_cast<unsigned int>(num));
|
||||
for (int i = 0; i < num; i++) {
|
||||
String ssid = WiFi.SSID(i);
|
||||
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
||||
int32_t rssi = WiFi.RSSI(i);
|
||||
uint8_t *bssid = WiFi.BSSID(i);
|
||||
int32_t channel = WiFi.channel(i);
|
||||
bool needs_full = this->needs_full_scan_results_();
|
||||
|
||||
this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]},
|
||||
std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN,
|
||||
ssid.length() == 0);
|
||||
// Access scan results directly via WiFi.scan struct to avoid Arduino String allocations
|
||||
// WiFi.scan is public in LibreTiny for WiFiEvents & WiFiScan static handlers
|
||||
auto *scan = WiFi.scan;
|
||||
|
||||
// First pass: count matching networks
|
||||
size_t count = 0;
|
||||
for (int i = 0; i < num; i++) {
|
||||
const char *ssid_cstr = scan->ap[i].ssid;
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
this->scan_result_.init(count); // Exact allocation
|
||||
|
||||
// Second pass: store matching networks
|
||||
for (int i = 0; i < num; i++) {
|
||||
const char *ssid_cstr = scan->ap[i].ssid;
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) {
|
||||
auto &ap = scan->ap[i];
|
||||
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
|
||||
ap.bssid.addr[4], ap.bssid.addr[5]},
|
||||
std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
|
||||
ssid_cstr[0] == '\0');
|
||||
} else {
|
||||
auto &ap = scan->ap[i];
|
||||
this->log_discarded_scan_result_(ssid_cstr, ap.bssid.addr, ap.rssi, ap.channel);
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Scan complete: %d found, %zu stored%s", num, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
WiFi.scanDelete();
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
|
||||
@@ -21,6 +21,7 @@ static const char *const TAG = "wifi_pico_w";
|
||||
// Track previous state for detecting changes
|
||||
static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static size_t s_scan_result_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
|
||||
if (sta.has_value()) {
|
||||
@@ -137,10 +138,20 @@ int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *r
|
||||
}
|
||||
|
||||
void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
|
||||
s_scan_result_count++;
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(result->ssid);
|
||||
|
||||
// Skip networks that don't match any configured network (unless full results needed)
|
||||
if (!this->needs_full_scan_results_() && !this->matches_configured_network_(ssid_cstr, result->bssid)) {
|
||||
this->log_discarded_scan_result_(ssid_cstr, result->bssid, result->rssi, result->channel);
|
||||
return;
|
||||
}
|
||||
|
||||
bssid_t bssid;
|
||||
std::copy(result->bssid, result->bssid + 6, bssid.begin());
|
||||
std::string ssid(reinterpret_cast<const char *>(result->ssid));
|
||||
WiFiScanResult res(bssid, ssid, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, ssid.empty());
|
||||
std::string ssid(ssid_cstr);
|
||||
WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN,
|
||||
ssid_cstr[0] == '\0');
|
||||
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
|
||||
this->scan_result_.push_back(res);
|
||||
}
|
||||
@@ -149,6 +160,7 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
|
||||
bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
this->scan_result_.clear();
|
||||
this->scan_done_ = false;
|
||||
s_scan_result_count = 0;
|
||||
cyw43_wifi_scan_options_t scan_options = {0};
|
||||
scan_options.scan_type = passive ? 1 : 0;
|
||||
int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result);
|
||||
@@ -244,7 +256,9 @@ void WiFiComponent::wifi_loop_() {
|
||||
// Handle scan completion
|
||||
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
|
||||
this->scan_done_ = true;
|
||||
ESP_LOGV(TAG, "Scan done");
|
||||
bool needs_full = this->needs_full_scan_results_();
|
||||
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const_zephyr import (
|
||||
CONF_IEEE802154_VENDOR_OUI,
|
||||
CONF_MAX_EP_NUMBER,
|
||||
CONF_ON_JOIN,
|
||||
CONF_POWER_SOURCE,
|
||||
@@ -58,6 +59,13 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_POWER_SOURCE, default="DC_SOURCE"): cv.enum(
|
||||
POWER_SOURCE, upper=True
|
||||
),
|
||||
cv.Optional(CONF_IEEE802154_VENDOR_OUI): cv.All(
|
||||
cv.Any(
|
||||
cv.int_range(min=0x000000, max=0xFFFFFF),
|
||||
cv.one_of(*["random"], lower=True),
|
||||
),
|
||||
cv.requires_component("nrf52"),
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
zigbee_set_core_data,
|
||||
@@ -120,7 +128,7 @@ async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
||||
def consume_endpoint(config: ConfigType) -> ConfigType:
|
||||
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
|
||||
return config
|
||||
if " " in config[CONF_NAME]:
|
||||
if CONF_NAME in config and " " in config[CONF_NAME]:
|
||||
_LOGGER.warning(
|
||||
"Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'",
|
||||
config[CONF_NAME],
|
||||
|
||||
@@ -22,6 +22,7 @@ POWER_SOURCE = {
|
||||
"EMERGENCY_MAINS_CONST": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_CONST",
|
||||
"EMERGENCY_MAINS_TRANSF": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_TRANSF",
|
||||
}
|
||||
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
|
||||
|
||||
# Keys for CORE.data storage
|
||||
KEY_ZIGBEE = "zigbee"
|
||||
|
||||
86
esphome/components/zigbee/time/__init__.py
Normal file
86
esphome/components/zigbee/time/__init__.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import consume_endpoint
|
||||
from ..const_zephyr import CONF_ZIGBEE_ID, zigbee_ns
|
||||
from ..zigbee_zephyr import (
|
||||
ZigbeeClusterDesc,
|
||||
ZigbeeComponent,
|
||||
get_slot_index,
|
||||
zigbee_new_attr_list,
|
||||
zigbee_new_cluster_list,
|
||||
zigbee_new_variable,
|
||||
zigbee_register_ep,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["zigbee"]
|
||||
|
||||
ZigbeeTime = zigbee_ns.class_("ZigbeeTime", time_.RealTimeClock)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
time_.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ZigbeeTime),
|
||||
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(
|
||||
ZigbeeComponent
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("1s")),
|
||||
consume_endpoint,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
CORE.add_job(_add_time, config)
|
||||
|
||||
|
||||
async def _add_time(config: ConfigType) -> None:
|
||||
slot_index = get_slot_index()
|
||||
|
||||
# Create unique names for this sensor's variables based on slot index
|
||||
prefix = f"zigbee_ep{slot_index + 1}"
|
||||
attrs_name = f"{prefix}_time_attrs"
|
||||
attr_list_name = f"{prefix}_time_attrib_list"
|
||||
cluster_list_name = f"{prefix}_cluster_list"
|
||||
ep_name = f"{prefix}_ep"
|
||||
|
||||
# Create the binary attributes structure
|
||||
time_attrs = zigbee_new_variable(attrs_name, "zb_zcl_time_attrs_t")
|
||||
attr_list = zigbee_new_attr_list(
|
||||
attr_list_name,
|
||||
"ZB_ZCL_DECLARE_TIME_ATTR_LIST",
|
||||
str(time_attrs),
|
||||
)
|
||||
|
||||
# Create cluster list and register endpoint
|
||||
cluster_list_name, clusters = zigbee_new_cluster_list(
|
||||
cluster_list_name,
|
||||
[
|
||||
ZigbeeClusterDesc("ZB_ZCL_CLUSTER_ID_TIME", attr_list),
|
||||
ZigbeeClusterDesc("ZB_ZCL_CLUSTER_ID_TIME"),
|
||||
],
|
||||
)
|
||||
zigbee_register_ep(
|
||||
ep_name,
|
||||
cluster_list_name,
|
||||
0,
|
||||
clusters,
|
||||
slot_index,
|
||||
"ZB_HA_CUSTOM_ATTR_DEVICE_ID",
|
||||
)
|
||||
|
||||
# Create the ZigbeeTime component
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await time_.register_time(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_endpoint(slot_index + 1))
|
||||
cg.add(var.set_cluster_attributes(time_attrs))
|
||||
hub = await cg.get_variable(config[CONF_ZIGBEE_ID])
|
||||
cg.add(var.set_parent(hub))
|
||||
87
esphome/components/zigbee/time/zigbee_time_zephyr.cpp
Normal file
87
esphome/components/zigbee/time/zigbee_time_zephyr.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "zigbee_time_zephyr.h"
|
||||
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME)
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
static const char *const TAG = "zigbee.time";
|
||||
|
||||
// This time standard is the number of
|
||||
// seconds since 0 hrs 0 mins 0 sec on 1st January 2000 UTC (Universal Coordinated Time).
|
||||
constexpr time_t EPOCH_2000 = 946684800;
|
||||
|
||||
ZigbeeTime *global_time = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void ZigbeeTime::sync_time(zb_ret_t status, zb_uint32_t auth_level, zb_uint16_t short_addr, zb_uint8_t endpoint,
|
||||
zb_uint32_t nw_time) {
|
||||
if (status == RET_OK && auth_level >= ZB_ZCL_TIME_HAS_SYNCHRONIZED_BIT) {
|
||||
global_time->set_epoch_time(nw_time + EPOCH_2000);
|
||||
} else if (status != RET_TIMEOUT || !global_time->has_time_) {
|
||||
ESP_LOGE(TAG, "Status: %d, auth_level: %u, short_addr: %d, endpoint: %d, nw_time: %u", status, auth_level,
|
||||
short_addr, endpoint, nw_time);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::setup() {
|
||||
global_time = this;
|
||||
this->parent_->add_callback(this->endpoint_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); });
|
||||
synchronize_epoch_(EPOCH_2000);
|
||||
this->parent_->add_join_callback([this]() { zb_zcl_time_server_synchronize(this->endpoint_, sync_time); });
|
||||
}
|
||||
|
||||
void ZigbeeTime::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Zigbee Time\n"
|
||||
" Endpoint: %d",
|
||||
this->endpoint_);
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
void ZigbeeTime::update() {
|
||||
time_t time = timestamp_now();
|
||||
this->cluster_attributes_->time = time - EPOCH_2000;
|
||||
}
|
||||
|
||||
void ZigbeeTime::set_epoch_time(uint32_t epoch) {
|
||||
this->defer([this, epoch]() {
|
||||
this->synchronize_epoch_(epoch);
|
||||
this->has_time_ = true;
|
||||
});
|
||||
}
|
||||
|
||||
void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) {
|
||||
zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t);
|
||||
zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id;
|
||||
zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id;
|
||||
zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id;
|
||||
|
||||
switch (device_cb_id) {
|
||||
/* ZCL set attribute value */
|
||||
case ZB_ZCL_SET_ATTR_VALUE_CB_ID:
|
||||
if (cluster_id == ZB_ZCL_CLUSTER_ID_TIME) {
|
||||
if (attr_id == ZB_ZCL_ATTR_TIME_TIME_ID) {
|
||||
zb_uint32_t value = p_device_cb_param->cb_param.set_attr_value_param.values.data32;
|
||||
ESP_LOGI(TAG, "Synchronize time to %u", value);
|
||||
this->defer([this, value]() { synchronize_epoch_(value + EPOCH_2000); });
|
||||
} else if (attr_id == ZB_ZCL_ATTR_TIME_TIME_STATUS_ID) {
|
||||
zb_uint8_t value = p_device_cb_param->cb_param.set_attr_value_param.values.data8;
|
||||
ESP_LOGI(TAG, "Time status %hd", value);
|
||||
this->defer([this, value]() { this->has_time_ = ZB_ZCL_TIME_TIME_STATUS_SYNCHRONIZED_BIT_IS_SET(value); });
|
||||
}
|
||||
} else {
|
||||
/* other clusters attribute handled here */
|
||||
ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id);
|
||||
p_device_cb_param->status = RET_NOT_IMPLEMENTED;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
p_device_cb_param->status = RET_NOT_IMPLEMENTED;
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Zcl_device_cb_ status: %hd", p_device_cb_param->status);
|
||||
}
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
|
||||
#endif
|
||||
38
esphome/components/zigbee/time/zigbee_time_zephyr.h
Normal file
38
esphome/components/zigbee/time/zigbee_time_zephyr.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME)
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#include "esphome/components/zigbee/zigbee_zephyr.h"
|
||||
|
||||
extern "C" {
|
||||
#include <zboss_api.h>
|
||||
#include <zboss_api_addons.h>
|
||||
}
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
class ZigbeeTime : public time::RealTimeClock, public ZigbeeEntity {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void set_cluster_attributes(zb_zcl_time_attrs_t &cluster_attributes) {
|
||||
this->cluster_attributes_ = &cluster_attributes;
|
||||
}
|
||||
|
||||
void set_epoch_time(uint32_t epoch);
|
||||
|
||||
protected:
|
||||
static void sync_time(zb_ret_t status, zb_uint32_t auth_level, zb_uint16_t short_addr, zb_uint8_t endpoint,
|
||||
zb_uint32_t nw_time);
|
||||
void zcl_device_cb_(zb_bufid_t bufid);
|
||||
zb_zcl_time_attrs_t *cluster_attributes_{nullptr};
|
||||
|
||||
bool has_time_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
|
||||
#endif
|
||||
@@ -22,7 +22,7 @@ void ZigbeeBinarySensor::setup() {
|
||||
ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
|
||||
ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value,
|
||||
ZB_FALSE);
|
||||
this->parent_->flush();
|
||||
this->parent_->force_report();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ void ZigbeeSensor::setup() {
|
||||
ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
|
||||
ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID,
|
||||
(zb_uint8_t *) &this->cluster_attributes_->present_value, ZB_FALSE);
|
||||
this->parent_->flush();
|
||||
this->parent_->force_report();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ void ZigbeeSwitch::setup() {
|
||||
ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
|
||||
ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value,
|
||||
ZB_FALSE);
|
||||
this->parent_->flush();
|
||||
this->parent_->force_report();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,8 +41,6 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) {
|
||||
zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id;
|
||||
zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id;
|
||||
|
||||
p_device_cb_param->status = RET_OK;
|
||||
|
||||
switch (device_cb_id) {
|
||||
/* ZCL set attribute value */
|
||||
case ZB_ZCL_SET_ATTR_VALUE_CB_ID:
|
||||
@@ -58,10 +56,11 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) {
|
||||
} else {
|
||||
/* other clusters attribute handled here */
|
||||
ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id);
|
||||
p_device_cb_param->status = RET_NOT_IMPLEMENTED;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
p_device_cb_param->status = RET_ERROR;
|
||||
p_device_cb_param->status = RET_NOT_IMPLEMENTED;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,10 +112,10 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
|
||||
const auto &cb = global_zigbee->callbacks_[endpoint - 1];
|
||||
if (cb) {
|
||||
cb(bufid);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
p_device_cb_param->status = RET_ERROR;
|
||||
p_device_cb_param->status = RET_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
void ZigbeeComponent::on_join_() {
|
||||
@@ -230,11 +230,11 @@ static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) {
|
||||
zb_buf_free(bufid);
|
||||
}
|
||||
|
||||
void ZigbeeComponent::flush() { this->need_flush_ = true; }
|
||||
void ZigbeeComponent::force_report() { this->force_report_ = true; }
|
||||
|
||||
void ZigbeeComponent::loop() {
|
||||
if (this->need_flush_) {
|
||||
this->need_flush_ = false;
|
||||
if (this->force_report_) {
|
||||
this->force_report_ = false;
|
||||
zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ class ZigbeeComponent : public Component {
|
||||
void zboss_signal_handler_esphome(zb_bufid_t bufid);
|
||||
void factory_reset();
|
||||
Trigger<> *get_join_trigger() { return &this->join_trigger_; };
|
||||
void flush();
|
||||
void force_report();
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
@@ -84,7 +84,7 @@ class ZigbeeComponent : public Component {
|
||||
std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{};
|
||||
CallbackManager<void()> join_cb_;
|
||||
Trigger<> join_trigger_;
|
||||
bool need_flush_{false};
|
||||
bool force_report_{false};
|
||||
};
|
||||
|
||||
class ZigbeeEntity {
|
||||
|
||||
@@ -49,6 +49,7 @@ from esphome.cpp_generator import (
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const_zephyr import (
|
||||
CONF_IEEE802154_VENDOR_OUI,
|
||||
CONF_ON_JOIN,
|
||||
CONF_POWER_SOURCE,
|
||||
CONF_WIPE_ON_BOOT,
|
||||
@@ -152,6 +153,13 @@ async def zephyr_to_code(config: ConfigType) -> None:
|
||||
zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False)
|
||||
zephyr_add_prj_conf("NET_UDP", False)
|
||||
|
||||
if CONF_IEEE802154_VENDOR_OUI in config:
|
||||
zephyr_add_prj_conf("IEEE802154_VENDOR_OUI_ENABLE", True)
|
||||
random_number = config[CONF_IEEE802154_VENDOR_OUI]
|
||||
if random_number == "random":
|
||||
random_number = random.randint(0x000000, 0xFFFFFF)
|
||||
zephyr_add_prj_conf("IEEE802154_VENDOR_OUI", random_number)
|
||||
|
||||
if config[CONF_WIPE_ON_BOOT]:
|
||||
if config[CONF_WIPE_ON_BOOT] == "once":
|
||||
cg.add_define(
|
||||
@@ -336,14 +344,14 @@ async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
|
||||
CORE.add_job(_add_switch, entity, config)
|
||||
|
||||
|
||||
def _slot_index() -> int:
|
||||
"""Find the next available endpoint slot"""
|
||||
def get_slot_index() -> int:
|
||||
"""Find the next available endpoint slot."""
|
||||
slot = next(
|
||||
(i for i, v in enumerate(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if v == ""), None
|
||||
)
|
||||
if slot is None:
|
||||
raise cv.Invalid(
|
||||
f"Not found empty slot, size ({len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])})"
|
||||
f"No available Zigbee endpoint slots ({len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])} in use)"
|
||||
)
|
||||
return slot
|
||||
|
||||
@@ -358,7 +366,7 @@ async def _add_zigbee_ep(
|
||||
app_device_id: str,
|
||||
extra_field_values: dict[str, int] | None = None,
|
||||
) -> None:
|
||||
slot_index = _slot_index()
|
||||
slot_index = get_slot_index()
|
||||
|
||||
prefix = f"zigbee_ep{slot_index + 1}"
|
||||
attrs_name = f"{prefix}_attrs"
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include <concepts>
|
||||
|
||||
#include <strings.h>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
@@ -245,6 +245,44 @@ cover:
|
||||
stop_action:
|
||||
- logger.log: stop_action
|
||||
optimistic: true
|
||||
- platform: template
|
||||
name: "Template Cover with Triggers"
|
||||
id: template_cover_with_triggers
|
||||
lambda: |-
|
||||
if (id(some_binary_sensor).state) {
|
||||
return COVER_OPEN;
|
||||
}
|
||||
return COVER_CLOSED;
|
||||
open_action:
|
||||
- logger.log: open_action
|
||||
close_action:
|
||||
- logger.log: close_action
|
||||
stop_action:
|
||||
- logger.log: stop_action
|
||||
optimistic: true
|
||||
on_open:
|
||||
- logger.log: "Cover on_open (deprecated)"
|
||||
on_opened:
|
||||
- logger.log: "Cover fully opened"
|
||||
on_closed:
|
||||
- logger.log: "Cover fully closed"
|
||||
on_opening:
|
||||
- logger.log: "Cover started opening"
|
||||
on_closing:
|
||||
- logger.log: "Cover started closing"
|
||||
on_idle:
|
||||
- logger.log: "Cover stopped moving"
|
||||
- logger.log: "Cover stopped moving"
|
||||
- if:
|
||||
condition:
|
||||
cover.is_open: template_cover_with_triggers
|
||||
then:
|
||||
logger.log: Cover is open
|
||||
- if:
|
||||
condition:
|
||||
cover.is_closed: template_cover_with_triggers
|
||||
then:
|
||||
logger.log: Cover is closed
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
|
||||
@@ -8,8 +8,6 @@ binary_sensor:
|
||||
name: "Garage Door Open 3"
|
||||
- platform: template
|
||||
name: "Garage Door Open 4"
|
||||
- platform: template
|
||||
name: "Garage Door Open 5"
|
||||
- platform: template
|
||||
name: "Garage Door Internal"
|
||||
internal: True
|
||||
@@ -21,6 +19,10 @@ sensor:
|
||||
- platform: template
|
||||
name: "Analog 2"
|
||||
lambda: return 11.0;
|
||||
- platform: template
|
||||
name: "Analog 3"
|
||||
lambda: return 12.0;
|
||||
internal: True
|
||||
|
||||
zigbee:
|
||||
wipe_on_boot: true
|
||||
@@ -35,6 +37,9 @@ output:
|
||||
write_action:
|
||||
- zigbee.factory_reset
|
||||
|
||||
time:
|
||||
- platform: zigbee
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
name: "Template Switch"
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
zigbee:
|
||||
wipe_on_boot: once
|
||||
power_source: battery
|
||||
ieee802154_vendor_oui: 0x231
|
||||
|
||||
Reference in New Issue
Block a user