mirror of
https://github.com/esphome/esphome.git
synced 2025-11-08 11:01:50 +00:00
Compare commits
29 Commits
select_opt
...
qualify_mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9b08491cc | ||
|
|
917deac7cb | ||
|
|
3d21adecd3 | ||
|
|
5b023f9369 | ||
|
|
6c2ce5cacf | ||
|
|
d23e25f099 | ||
|
|
9b78098eec | ||
|
|
7e5b82c5f3 | ||
|
|
2864e989bd | ||
|
|
6efe346cc5 | ||
|
|
f2f6c597ef | ||
|
|
b91b12d77a | ||
|
|
7f567bdfbe | ||
|
|
f2de8df556 | ||
|
|
1c67a61945 | ||
|
|
77141d3e83 | ||
|
|
f592f79bce | ||
|
|
6edbb94529 | ||
|
|
d37eb59fd7 | ||
|
|
e2b3617df3 | ||
|
|
e1c851cab8 | ||
|
|
146b067d62 | ||
|
|
5b15827009 | ||
|
|
0de79ba291 | ||
|
|
e3aaf6a144 | ||
|
|
78ffeb30fb | ||
|
|
2c1927fd12 | ||
|
|
c6ae1a5909 | ||
|
|
9c712744be |
@@ -201,6 +201,7 @@ esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
esphome/components/hbridge/switch/* @dwmw2
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect";
|
||||
static const uint32_t ADALIGHT_ACK_INTERVAL = 1000;
|
||||
static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000;
|
||||
|
||||
AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
|
||||
void AdalightLightEffect::start() {
|
||||
AddressableLightEffect::start();
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace adalight {
|
||||
|
||||
class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice {
|
||||
public:
|
||||
AdalightLightEffect(const std::string &name);
|
||||
AdalightLightEffect(const char *name);
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
|
||||
@@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
if (light->supports_effects()) {
|
||||
msg.effects.emplace_back("None");
|
||||
for (auto *effect : light->get_effects()) {
|
||||
msg.effects.push_back(effect->get_name());
|
||||
msg.effects.emplace_back(effect->get_name());
|
||||
}
|
||||
}
|
||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
@@ -1572,7 +1572,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
|
||||
resp.success = false;
|
||||
|
||||
psk_t psk{};
|
||||
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
if (msg.key.empty()) {
|
||||
if (this->parent_->clear_noise_psk(true)) {
|
||||
resp.success = true;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to clear encryption key");
|
||||
}
|
||||
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
|
||||
@@ -468,6 +468,31 @@ uint16_t APIServer::get_port() const { return this->port_; }
|
||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
||||
const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) {
|
||||
if (!this->noise_pref_.save(&new_psk)) {
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
|
||||
return false;
|
||||
}
|
||||
// ensure it's written immediately
|
||||
if (!global_preferences->sync()) {
|
||||
ESP_LOGW(TAG, "Failed to sync preferences");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
|
||||
if (make_active) {
|
||||
this->set_timeout(100, [this, active_psk]() {
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
||||
this->set_noise_psk(active_psk);
|
||||
for (auto &c : this->clients_) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||
// When PSK is set from YAML, this function should never be called
|
||||
@@ -482,27 +507,21 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
}
|
||||
|
||||
SavedNoisePsk new_saved_psk{psk};
|
||||
if (!this->noise_pref_.save(&new_saved_psk)) {
|
||||
ESP_LOGW(TAG, "Failed to save Noise PSK");
|
||||
return false;
|
||||
}
|
||||
// ensure it's written immediately
|
||||
if (!global_preferences->sync()) {
|
||||
ESP_LOGW(TAG, "Failed to sync preferences");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Noise PSK saved");
|
||||
if (make_active) {
|
||||
this->set_timeout(100, [this, psk]() {
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
||||
this->set_noise_psk(psk);
|
||||
for (auto &c : this->clients_) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk,
|
||||
make_active);
|
||||
#endif
|
||||
}
|
||||
bool APIServer::clear_noise_psk(bool make_active) {
|
||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||
// When PSK is set from YAML, this function should never be called
|
||||
// but if it is, reject the change
|
||||
ESP_LOGW(TAG, "Key set in YAML");
|
||||
return false;
|
||||
#else
|
||||
SavedNoisePsk empty_psk{};
|
||||
psk_t empty{};
|
||||
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty,
|
||||
make_active);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -53,6 +53,7 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
bool clear_noise_psk(bool make_active = true);
|
||||
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
|
||||
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
@@ -174,6 +175,10 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
protected:
|
||||
void schedule_reboot_timeout_();
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
#endif // USE_API_NOISE
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#include <cinttypes>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> {
|
||||
|
||||
class MultiClickTrigger : public Trigger<>, public Component {
|
||||
public:
|
||||
explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing)
|
||||
: parent_(parent), timing_(std::move(timing)) {}
|
||||
explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
|
||||
: parent_(parent), timing_(timing) {}
|
||||
|
||||
void setup() override {
|
||||
this->last_state_ = this->parent_->get_state_default(false);
|
||||
@@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
|
||||
void trigger_();
|
||||
|
||||
BinarySensor *parent_;
|
||||
std::vector<MultiClickTriggerEvent> timing_;
|
||||
FixedVector<MultiClickTriggerEvent> timing_;
|
||||
uint32_t invalid_cooldown_{1000};
|
||||
optional<size_t> at_index_{};
|
||||
bool last_state_{false};
|
||||
|
||||
@@ -524,13 +524,23 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
call.set_target_humidity(this->target_humidity);
|
||||
}
|
||||
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
|
||||
if (this->uses_custom_fan_mode) {
|
||||
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
||||
call.fan_mode_.reset();
|
||||
call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
|
||||
}
|
||||
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
||||
call.set_fan_mode(this->fan_mode);
|
||||
}
|
||||
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
|
||||
if (this->uses_custom_preset) {
|
||||
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
||||
call.preset_.reset();
|
||||
call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
|
||||
}
|
||||
} else if (traits.supports_preset(this->preset)) {
|
||||
call.set_preset(this->preset);
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
if (traits.supports_swing_mode(this->swing_mode)) {
|
||||
call.set_swing_mode(this->swing_mode);
|
||||
}
|
||||
return call;
|
||||
@@ -549,41 +559,25 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||
climate->target_humidity = this->target_humidity;
|
||||
}
|
||||
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
|
||||
if (this->uses_custom_fan_mode) {
|
||||
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
||||
climate->fan_mode.reset();
|
||||
climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
|
||||
}
|
||||
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
||||
climate->fan_mode = this->fan_mode;
|
||||
climate->custom_fan_mode.reset();
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
|
||||
// std::set has consistent order (lexicographic for strings)
|
||||
const auto &modes = traits.get_supported_custom_fan_modes();
|
||||
if (custom_fan_mode < modes.size()) {
|
||||
size_t i = 0;
|
||||
for (const auto &mode : modes) {
|
||||
if (i == this->custom_fan_mode) {
|
||||
climate->custom_fan_mode = mode;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (this->uses_custom_preset) {
|
||||
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
||||
climate->preset.reset();
|
||||
climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_presets() && !this->uses_custom_preset) {
|
||||
} else if (traits.supports_preset(this->preset)) {
|
||||
climate->preset = this->preset;
|
||||
climate->custom_preset.reset();
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
|
||||
// std::set has consistent order (lexicographic for strings)
|
||||
const auto &presets = traits.get_supported_custom_presets();
|
||||
if (custom_preset < presets.size()) {
|
||||
size_t i = 0;
|
||||
for (const auto &preset : presets) {
|
||||
if (i == this->custom_preset) {
|
||||
climate->custom_preset = preset;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
if (traits.supports_swing_mode(this->swing_mode)) {
|
||||
climate->swing_mode = this->swing_mode;
|
||||
}
|
||||
climate->publish_state();
|
||||
|
||||
@@ -33,6 +33,7 @@ class Climate;
|
||||
class ClimateCall {
|
||||
public:
|
||||
explicit ClimateCall(Climate *parent) : parent_(parent) {}
|
||||
friend struct ClimateDeviceRestoreState;
|
||||
|
||||
/// Set the mode of the climate device.
|
||||
ClimateCall &set_mode(ClimateMode mode);
|
||||
|
||||
@@ -80,8 +80,8 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
|
||||
light_effect->get_first_universe(), light_effect->get_last_universe());
|
||||
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||
light_effect->get_last_universe());
|
||||
|
||||
light_effects_.insert(light_effect);
|
||||
|
||||
@@ -95,8 +95,8 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
|
||||
light_effect->get_first_universe(), light_effect->get_last_universe());
|
||||
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||
light_effect->get_last_universe());
|
||||
|
||||
light_effects_.erase(light_effect);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace e131 {
|
||||
static const char *const TAG = "e131_addressable_light_effect";
|
||||
static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1);
|
||||
|
||||
E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
|
||||
int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; }
|
||||
|
||||
@@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
|
||||
std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1));
|
||||
auto *input_data = packet.values + 1;
|
||||
|
||||
ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe,
|
||||
output_offset, output_end);
|
||||
ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset,
|
||||
output_end);
|
||||
|
||||
switch (channels_) {
|
||||
case E131_MONO:
|
||||
|
||||
@@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 };
|
||||
|
||||
class E131AddressableLightEffect : public light::AddressableLightEffect {
|
||||
public:
|
||||
E131AddressableLightEffect(const std::string &name);
|
||||
E131AddressableLightEffect(const char *name);
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
|
||||
@@ -877,6 +877,11 @@ async def to_code(config):
|
||||
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
||||
os.environ.pop(clean_var, None)
|
||||
|
||||
# Set the location of the IDF component manager cache
|
||||
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
add_extra_script(
|
||||
"post",
|
||||
"post_build.py",
|
||||
|
||||
@@ -32,6 +32,7 @@ from esphome.const import (
|
||||
CONF_MISO_PIN,
|
||||
CONF_MODE,
|
||||
CONF_MOSI_PIN,
|
||||
CONF_NUMBER,
|
||||
CONF_PAGE_ID,
|
||||
CONF_PIN,
|
||||
CONF_POLLING_INTERVAL,
|
||||
@@ -52,12 +53,36 @@ from esphome.core import (
|
||||
coroutine_with_priority,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONFLICTS_WITH = ["wifi"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
AUTO_LOAD = ["network"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# RMII pins that are hardcoded on ESP32 classic and cannot be changed
|
||||
# These pins are used by the internal Ethernet MAC when using RMII PHYs
|
||||
ESP32_RMII_FIXED_PINS = {
|
||||
19: "EMAC_TXD0",
|
||||
21: "EMAC_TX_EN",
|
||||
22: "EMAC_TXD1",
|
||||
25: "EMAC_RXD0",
|
||||
26: "EMAC_RXD1",
|
||||
27: "EMAC_RX_CRS_DV",
|
||||
}
|
||||
|
||||
# RMII default pins for ESP32-P4
|
||||
# These are the default pins used by ESP-IDF and are configurable in principle,
|
||||
# but ESPHome's ethernet component currently has no way to change them
|
||||
ESP32P4_RMII_DEFAULT_PINS = {
|
||||
34: "EMAC_TXD0",
|
||||
35: "EMAC_TXD1",
|
||||
28: "EMAC_RX_CRS_DV",
|
||||
29: "EMAC_RXD0",
|
||||
30: "EMAC_RXD1",
|
||||
49: "EMAC_TX_EN",
|
||||
}
|
||||
|
||||
ethernet_ns = cg.esphome_ns.namespace("ethernet")
|
||||
PHYRegister = ethernet_ns.struct("PHYRegister")
|
||||
CONF_PHY_ADDR = "phy_addr"
|
||||
@@ -273,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
def _final_validate_spi(config):
|
||||
if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
|
||||
return
|
||||
if spi_configs := fv.full_config.get().get(CONF_SPI):
|
||||
@@ -292,9 +317,6 @@ def _final_validate(config):
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def manual_ip(config):
|
||||
return cg.StructInitializer(
|
||||
ManualIP,
|
||||
@@ -383,3 +405,57 @@ async def to_code(config):
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
|
||||
def _final_validate_rmii_pins(config: ConfigType) -> None:
|
||||
"""Validate that RMII pins are not used by other components."""
|
||||
# Only validate for RMII-based PHYs on ESP32/ESP32P4
|
||||
if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH":
|
||||
return # SPI and OPENETH don't use RMII
|
||||
|
||||
variant = get_esp32_variant()
|
||||
if variant == VARIANT_ESP32:
|
||||
rmii_pins = ESP32_RMII_FIXED_PINS
|
||||
is_configurable = False
|
||||
elif variant == VARIANT_ESP32P4:
|
||||
rmii_pins = ESP32P4_RMII_DEFAULT_PINS
|
||||
is_configurable = True
|
||||
else:
|
||||
return # No RMII validation needed for other variants
|
||||
|
||||
# Check all used pins against RMII reserved pins
|
||||
for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values():
|
||||
for pin_path, _, pin_config in pin_list:
|
||||
pin_num = pin_config.get(CONF_NUMBER)
|
||||
if pin_num not in rmii_pins:
|
||||
continue
|
||||
# Found a conflict - show helpful error message
|
||||
pin_function = rmii_pins[pin_num]
|
||||
component_path = ".".join(str(p) for p in pin_path)
|
||||
if is_configurable:
|
||||
error_msg = (
|
||||
f"GPIO{pin_num} is used by Ethernet RMII "
|
||||
f"({pin_function}) with the current default "
|
||||
f"configuration. This conflicts with '{component_path}'. "
|
||||
f"Please choose a different GPIO pin for "
|
||||
f"'{component_path}'."
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
f"GPIO{pin_num} is reserved for Ethernet RMII "
|
||||
f"({pin_function}) and cannot be used. This pin is "
|
||||
f"hardcoded by ESP-IDF and cannot be changed when using "
|
||||
f"RMII Ethernet PHYs. Please choose a different GPIO pin "
|
||||
f"for '{component_path}'."
|
||||
)
|
||||
raise cv.Invalid(error_msg, path=pin_path)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
"""Final validation for Ethernet component."""
|
||||
_final_validate_spi(config)
|
||||
_final_validate_rmii_pins(config)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
@@ -8,12 +8,19 @@ namespace event {
|
||||
static const char *const TAG = "event";
|
||||
|
||||
void Event::trigger(const std::string &event_type) {
|
||||
auto found = types_.find(event_type);
|
||||
if (found == types_.end()) {
|
||||
// Linear search - faster than std::set for small datasets (1-5 items typical)
|
||||
const std::string *found = nullptr;
|
||||
for (const auto &type : this->types_) {
|
||||
if (type == event_type) {
|
||||
found = &type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == nullptr) {
|
||||
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
|
||||
return;
|
||||
}
|
||||
last_event_type = &(*found);
|
||||
last_event_type = found;
|
||||
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str());
|
||||
this->event_callback_.call(event_type);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
@@ -26,13 +25,13 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
|
||||
const std::string *last_event_type;
|
||||
|
||||
void trigger(const std::string &event_type);
|
||||
void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; }
|
||||
std::set<std::string> get_event_types() const { return this->types_; }
|
||||
void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; }
|
||||
const FixedVector<std::string> &get_event_types() const { return this->types_; }
|
||||
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
|
||||
|
||||
protected:
|
||||
CallbackManager<void(const std::string &event_type)> event_callback_;
|
||||
std::set<std::string> types_;
|
||||
FixedVector<std::string> types_;
|
||||
};
|
||||
|
||||
} // namespace event
|
||||
|
||||
1
esphome/components/hdc2010/__init__.py
Normal file
1
esphome/components/hdc2010/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@optimusprimespace", "@ssieb"]
|
||||
111
esphome/components/hdc2010/hdc2010.cpp
Normal file
111
esphome/components/hdc2010/hdc2010.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "hdc2010.h"
|
||||
// https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js
|
||||
// https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp
|
||||
namespace esphome {
|
||||
namespace hdc2010 {
|
||||
|
||||
static const char *const TAG = "hdc2010";
|
||||
|
||||
static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet
|
||||
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
|
||||
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
|
||||
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
|
||||
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
|
||||
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
|
||||
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
|
||||
static const uint8_t CONFIG = 0x0E;
|
||||
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
|
||||
|
||||
void HDC2010Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
const uint8_t data[2] = {
|
||||
0b00000000, // resolution 14bit for both humidity and temperature
|
||||
0b00000000 // reserved
|
||||
};
|
||||
|
||||
if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
|
||||
ESP_LOGW(TAG, "Initial config instruction error");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set measurement mode to temperature and humidity
|
||||
uint8_t config_contents;
|
||||
this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode
|
||||
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
|
||||
// Set rate to manual
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0x8F;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
|
||||
// Set temperature resolution to 14bit
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0x3F;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
|
||||
// Set humidity resolution to 14bit
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0xCF;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
}
|
||||
|
||||
void HDC2010Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HDC2010:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
void HDC2010Component::update() {
|
||||
// Trigger measurement
|
||||
uint8_t config_contents;
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents |= 0x01;
|
||||
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
|
||||
// 1ms delay after triggering the sample
|
||||
set_timeout(1, [this]() {
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
float temp = this->read_temp();
|
||||
this->temperature_sensor_->publish_state(temp);
|
||||
ESP_LOGD(TAG, "Temp=%.1f°C", temp);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
float humidity = this->read_humidity();
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
ESP_LOGD(TAG, "Humidity=%.1f%%", humidity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
float HDC2010Component::read_temp() {
|
||||
uint8_t byte[2];
|
||||
|
||||
this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
|
||||
this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
|
||||
|
||||
uint16_t temp = encode_uint16(byte[1], byte[0]);
|
||||
return (float) temp * 0.0025177f - 40.0f;
|
||||
}
|
||||
|
||||
float HDC2010Component::read_humidity() {
|
||||
uint8_t byte[2];
|
||||
|
||||
this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
|
||||
this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
|
||||
|
||||
uint16_t humidity = encode_uint16(byte[1], byte[0]);
|
||||
return (float) humidity * 0.001525879f;
|
||||
}
|
||||
|
||||
} // namespace hdc2010
|
||||
} // namespace esphome
|
||||
32
esphome/components/hdc2010/hdc2010.h
Normal file
32
esphome/components/hdc2010/hdc2010.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hdc2010 {
|
||||
|
||||
class HDC2010Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; }
|
||||
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; }
|
||||
|
||||
/// Setup the sensor and check for connection.
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
/// Retrieve the latest sensor values. This operation takes approximately 16ms.
|
||||
void update() override;
|
||||
|
||||
float read_temp();
|
||||
|
||||
float read_humidity();
|
||||
|
||||
protected:
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace hdc2010
|
||||
} // namespace esphome
|
||||
56
esphome/components/hdc2010/sensor.py
Normal file
56
esphome/components/hdc2010/sensor.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
hdc2010_ns = cg.esphome_ns.namespace("hdc2010")
|
||||
HDC2010Component = hdc2010_ns.class_(
|
||||
"HDC2010Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HDC2010Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x40))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
@@ -28,6 +28,38 @@ void ImprovSerialComponent::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::loop() {
|
||||
if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
|
||||
this->last_read_byte_ = 0;
|
||||
this->rx_buffer_.clear();
|
||||
ESP_LOGV(TAG, "Timeout");
|
||||
}
|
||||
|
||||
auto byte = this->read_byte_();
|
||||
while (byte.has_value()) {
|
||||
if (this->parse_improv_serial_byte_(byte.value())) {
|
||||
this->last_read_byte_ = millis();
|
||||
} else {
|
||||
this->last_read_byte_ = 0;
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
byte = this->read_byte_();
|
||||
}
|
||||
|
||||
if (this->state_ == improv::STATE_PROVISIONING) {
|
||||
if (wifi::global_wifi_component->is_connected()) {
|
||||
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
|
||||
this->connecting_sta_.get_password());
|
||||
this->connecting_sta_ = {};
|
||||
this->cancel_timeout("wifi-connect-timeout");
|
||||
this->set_state_(improv::STATE_PROVISIONED);
|
||||
|
||||
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
|
||||
this->send_response_(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
|
||||
|
||||
optional<uint8_t> ImprovSerialComponent::read_byte_() {
|
||||
@@ -78,8 +110,28 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() {
|
||||
return byte;
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
|
||||
data.push_back('\n');
|
||||
void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
|
||||
// First, set length field
|
||||
this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
|
||||
|
||||
const bool there_is_data = data != nullptr && size > 0;
|
||||
// If there_is_data, checksum must not include our optional data byte
|
||||
const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2;
|
||||
// Only transmit the full buffer length if there is no data (only state/error byte is provided in this case)
|
||||
const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE;
|
||||
// Calculate checksum for message
|
||||
uint8_t checksum = 0;
|
||||
for (uint8_t i = 0; i < header_checksum_len; i++) {
|
||||
checksum += this->tx_header_[i];
|
||||
}
|
||||
if (there_is_data) {
|
||||
// Include data in checksum
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
checksum += data[i];
|
||||
}
|
||||
}
|
||||
this->tx_header_[TX_CHECKSUM_IDX] = checksum;
|
||||
|
||||
#ifdef USE_ESP32
|
||||
switch (logger::global_logger->get_uart()) {
|
||||
case logger::UART_SELECTION_UART0:
|
||||
@@ -87,63 +139,45 @@ void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
case logger::UART_SELECTION_UART2:
|
||||
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
|
||||
uart_write_bytes(this->uart_num_, data.data(), data.size());
|
||||
#endif
|
||||
uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len);
|
||||
if (there_is_data) {
|
||||
uart_write_bytes(this->uart_num_, data, size);
|
||||
uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
|
||||
}
|
||||
break;
|
||||
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
|
||||
case logger::UART_SELECTION_USB_CDC: {
|
||||
const char *msg = (char *) data.data();
|
||||
esp_usb_console_write_buf(msg, data.size());
|
||||
case logger::UART_SELECTION_USB_CDC:
|
||||
esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len);
|
||||
if (there_is_data) {
|
||||
esp_usb_console_write_buf((const char *) data, size);
|
||||
esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX],
|
||||
2); // Footer: checksum and newline
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif // USE_LOGGER_USB_CDC
|
||||
#endif
|
||||
#ifdef USE_LOGGER_USB_SERIAL_JTAG
|
||||
case logger::UART_SELECTION_USB_SERIAL_JTAG:
|
||||
usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS);
|
||||
delay(10);
|
||||
usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7
|
||||
usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS);
|
||||
if (there_is_data) {
|
||||
usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS);
|
||||
usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2,
|
||||
20 / portTICK_PERIOD_MS); // Footer: checksum and newline
|
||||
}
|
||||
break;
|
||||
#endif // USE_LOGGER_USB_SERIAL_JTAG
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#elif defined(USE_ARDUINO)
|
||||
this->hw_serial_->write(data.data(), data.size());
|
||||
this->hw_serial_->write(this->tx_header_, header_tx_len);
|
||||
if (there_is_data) {
|
||||
this->hw_serial_->write(data, size);
|
||||
this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::loop() {
|
||||
if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
|
||||
this->last_read_byte_ = 0;
|
||||
this->rx_buffer_.clear();
|
||||
ESP_LOGV(TAG, "Improv Serial timeout");
|
||||
}
|
||||
|
||||
auto byte = this->read_byte_();
|
||||
while (byte.has_value()) {
|
||||
if (this->parse_improv_serial_byte_(byte.value())) {
|
||||
this->last_read_byte_ = millis();
|
||||
} else {
|
||||
this->last_read_byte_ = 0;
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
byte = this->read_byte_();
|
||||
}
|
||||
|
||||
if (this->state_ == improv::STATE_PROVISIONING) {
|
||||
if (wifi::global_wifi_component->is_connected()) {
|
||||
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
|
||||
this->connecting_sta_.get_password());
|
||||
this->connecting_sta_ = {};
|
||||
this->cancel_timeout("wifi-connect-timeout");
|
||||
this->set_state_(improv::STATE_PROVISIONED);
|
||||
|
||||
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
|
||||
this->send_response_(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
|
||||
std::vector<std::string> urls;
|
||||
#ifdef USE_IMPROV_SERIAL_NEXT_URL
|
||||
@@ -177,13 +211,13 @@ std::vector<uint8_t> ImprovSerialComponent::build_version_info_() {
|
||||
bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
|
||||
size_t at = this->rx_buffer_.size();
|
||||
this->rx_buffer_.push_back(byte);
|
||||
ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte);
|
||||
ESP_LOGV(TAG, "Byte: 0x%02X", byte);
|
||||
const uint8_t *raw = &this->rx_buffer_[0];
|
||||
|
||||
return improv::parse_improv_serial_byte(
|
||||
at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
|
||||
[this](improv::Error error) -> void {
|
||||
ESP_LOGW(TAG, "Error decoding Improv payload");
|
||||
ESP_LOGW(TAG, "Error decoding payload");
|
||||
this->set_error_(error);
|
||||
});
|
||||
}
|
||||
@@ -199,7 +233,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
wifi::global_wifi_component->start_connecting(sta, false);
|
||||
this->set_state_(improv::STATE_PROVISIONING);
|
||||
ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this);
|
||||
@@ -240,7 +274,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
ESP_LOGW(TAG, "Unknown Improv payload");
|
||||
ESP_LOGW(TAG, "Unknown payload");
|
||||
this->set_error_(improv::ERROR_UNKNOWN_RPC);
|
||||
return false;
|
||||
}
|
||||
@@ -249,57 +283,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
|
||||
void ImprovSerialComponent::set_state_(improv::State state) {
|
||||
this->state_ = state;
|
||||
|
||||
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
|
||||
data.resize(11);
|
||||
data[6] = IMPROV_SERIAL_VERSION;
|
||||
data[7] = TYPE_CURRENT_STATE;
|
||||
data[8] = 1;
|
||||
data[9] = state;
|
||||
|
||||
uint8_t checksum = 0x00;
|
||||
for (uint8_t d : data)
|
||||
checksum += d;
|
||||
data[10] = checksum;
|
||||
|
||||
this->write_data_(data);
|
||||
this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE;
|
||||
this->tx_header_[TX_DATA_IDX] = state;
|
||||
this->write_data_();
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::set_error_(improv::Error error) {
|
||||
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
|
||||
data.resize(11);
|
||||
data[6] = IMPROV_SERIAL_VERSION;
|
||||
data[7] = TYPE_ERROR_STATE;
|
||||
data[8] = 1;
|
||||
data[9] = error;
|
||||
|
||||
uint8_t checksum = 0x00;
|
||||
for (uint8_t d : data)
|
||||
checksum += d;
|
||||
data[10] = checksum;
|
||||
this->write_data_(data);
|
||||
this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE;
|
||||
this->tx_header_[TX_DATA_IDX] = error;
|
||||
this->write_data_();
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) {
|
||||
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
|
||||
data.resize(9);
|
||||
data[6] = IMPROV_SERIAL_VERSION;
|
||||
data[7] = TYPE_RPC_RESPONSE;
|
||||
data[8] = response.size();
|
||||
data.insert(data.end(), response.begin(), response.end());
|
||||
|
||||
uint8_t checksum = 0x00;
|
||||
for (uint8_t d : data)
|
||||
checksum += d;
|
||||
data.push_back(checksum);
|
||||
|
||||
this->write_data_(data);
|
||||
this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE;
|
||||
this->write_data_(response.data(), response.size());
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::on_wifi_connect_timeout_() {
|
||||
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
|
||||
this->set_state_(improv::STATE_AUTHORIZED);
|
||||
ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network");
|
||||
ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
|
||||
wifi::global_wifi_component->clear_sta();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,16 @@
|
||||
namespace esphome {
|
||||
namespace improv_serial {
|
||||
|
||||
// TX buffer layout constants
|
||||
static constexpr uint8_t TX_HEADER_SIZE = 6; // Bytes 0-5 = "IMPROV"
|
||||
static constexpr uint8_t TX_VERSION_IDX = 6;
|
||||
static constexpr uint8_t TX_TYPE_IDX = 7;
|
||||
static constexpr uint8_t TX_LENGTH_IDX = 8;
|
||||
static constexpr uint8_t TX_DATA_IDX = 9; // For state/error messages only
|
||||
static constexpr uint8_t TX_CHECKSUM_IDX = 10;
|
||||
static constexpr uint8_t TX_NEWLINE_IDX = 11;
|
||||
static constexpr uint8_t TX_BUFFER_SIZE = 12;
|
||||
|
||||
enum ImprovSerialType : uint8_t {
|
||||
TYPE_CURRENT_STATE = 0x01,
|
||||
TYPE_ERROR_STATE = 0x02,
|
||||
@@ -57,7 +67,22 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
|
||||
std::vector<uint8_t> build_version_info_();
|
||||
|
||||
optional<uint8_t> read_byte_();
|
||||
void write_data_(std::vector<uint8_t> &data);
|
||||
void write_data_(const uint8_t *data = nullptr, size_t size = 0);
|
||||
|
||||
uint8_t tx_header_[TX_BUFFER_SIZE] = {
|
||||
'I', // 0: Header
|
||||
'M', // 1: Header
|
||||
'P', // 2: Header
|
||||
'R', // 3: Header
|
||||
'O', // 4: Header
|
||||
'V', // 5: Header
|
||||
IMPROV_SERIAL_VERSION, // 6: Version
|
||||
0, // 7: ImprovSerialType
|
||||
0, // 8: Length
|
||||
0, // 9...X: Data (here, one byte reserved for state/error)
|
||||
0, // X + 10: Checksum
|
||||
'\n',
|
||||
};
|
||||
|
||||
#ifdef USE_ESP32
|
||||
uart_port_t uart_num_;
|
||||
|
||||
@@ -61,6 +61,10 @@ void AddressableLightTransformer::start() {
|
||||
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
|
||||
}
|
||||
|
||||
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
|
||||
return uint8_t(int32_t(a) - (((int32_t(a) - int32_t(b)) * scale) / 256));
|
||||
}
|
||||
|
||||
optional<LightColorValues> AddressableLightTransformer::apply() {
|
||||
float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_());
|
||||
|
||||
@@ -74,38 +78,37 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
|
||||
// all LEDs, we use the current state of each LED as the start.
|
||||
|
||||
// We can't use a direct lerp smoothing here though - that would require creating a copy of the original
|
||||
// state of each LED at the start of the transition.
|
||||
// Instead, we "fake" the look of the LERP by using an exponential average over time and using
|
||||
// dynamically-calculated alpha values to match the look.
|
||||
// state of each LED at the start of the transition. Instead, we "fake" the look of lerp by calculating
|
||||
// the delta between the current state and the target state, assuming that the delta represents the rest
|
||||
// of the transition that was to be applied as of the previous transition step, and scaling the delta for
|
||||
// what should be left after the current transition step. In this manner, the delta decays to zero as the
|
||||
// transition progresses.
|
||||
//
|
||||
// Here's an example of how the algorithm progresses in discrete steps:
|
||||
//
|
||||
// At time = 0.00, 0% complete, 100% remaining, 100% will remain after this step, so the scale is 100% / 100% = 100%.
|
||||
// At time = 0.10, 0% complete, 100% remaining, 90% will remain after this step, so the scale is 90% / 100% = 90%.
|
||||
// At time = 0.20, 10% complete, 90% remaining, 80% will remain after this step, so the scale is 80% / 90% = 88.9%.
|
||||
// At time = 0.50, 20% complete, 80% remaining, 50% will remain after this step, so the scale is 50% / 80% = 62.5%.
|
||||
// At time = 0.90, 50% complete, 50% remaining, 10% will remain after this step, so the scale is 10% / 50% = 20%.
|
||||
// At time = 0.91, 90% complete, 10% remaining, 9% will remain after this step, so the scale is 9% / 10% = 90%.
|
||||
// At time = 1.00, 91% complete, 9% remaining, 0% will remain after this step, so the scale is 0% / 9% = 0%.
|
||||
//
|
||||
// Because the color values are quantized to 8 bit resolution after each step, the transition may appear
|
||||
// non-linear when applying small deltas.
|
||||
|
||||
float denom = (1.0f - smoothed_progress);
|
||||
float alpha = denom == 0.0f ? 1.0f : (smoothed_progress - this->last_transition_progress_) / denom;
|
||||
|
||||
// We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length
|
||||
// We solve this by accumulating the fractional part of the alpha over time.
|
||||
float alpha255 = alpha * 255.0f;
|
||||
float alpha255int = floorf(alpha255);
|
||||
float alpha255remainder = alpha255 - alpha255int;
|
||||
|
||||
this->accumulated_alpha_ += alpha255remainder;
|
||||
float alpha_add = floorf(this->accumulated_alpha_);
|
||||
this->accumulated_alpha_ -= alpha_add;
|
||||
|
||||
alpha255 += alpha_add;
|
||||
alpha255 = clamp(alpha255, 0.0f, 255.0f);
|
||||
auto alpha8 = static_cast<uint8_t>(alpha255);
|
||||
|
||||
if (alpha8 != 0) {
|
||||
uint8_t inv_alpha8 = 255 - alpha8;
|
||||
Color add = this->target_color_ * alpha8;
|
||||
|
||||
for (auto led : this->light_)
|
||||
led.set(add + led.get() * inv_alpha8);
|
||||
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
|
||||
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
|
||||
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
|
||||
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
|
||||
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
|
||||
}
|
||||
this->last_transition_progress_ = smoothed_progress;
|
||||
this->light_.schedule_show();
|
||||
}
|
||||
|
||||
this->last_transition_progress_ = smoothed_progress;
|
||||
this->light_.schedule_show();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ class AddressableLightTransformer : public LightTransformer {
|
||||
protected:
|
||||
AddressableLight &light_;
|
||||
float last_transition_progress_{0.0f};
|
||||
float accumulated_alpha_{0.0f};
|
||||
Color target_color_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/light/light_state.h"
|
||||
#include "esphome/components/light/addressable_light.h"
|
||||
|
||||
@@ -30,7 +30,7 @@ inline static uint8_t half_sin8(uint8_t v) { return sin16_c(uint16_t(v) * 128u)
|
||||
|
||||
class AddressableLightEffect : public LightEffect {
|
||||
public:
|
||||
explicit AddressableLightEffect(const std::string &name) : LightEffect(name) {}
|
||||
explicit AddressableLightEffect(const char *name) : LightEffect(name) {}
|
||||
void start_internal() override {
|
||||
this->get_addressable_()->set_effect_active(true);
|
||||
this->get_addressable_()->clear_effect_data();
|
||||
@@ -57,8 +57,7 @@ class AddressableLightEffect : public LightEffect {
|
||||
|
||||
class AddressableLambdaLightEffect : public AddressableLightEffect {
|
||||
public:
|
||||
AddressableLambdaLightEffect(const std::string &name,
|
||||
std::function<void(AddressableLight &, Color, bool initial_run)> f,
|
||||
AddressableLambdaLightEffect(const char *name, std::function<void(AddressableLight &, Color, bool initial_run)> f,
|
||||
uint32_t update_interval)
|
||||
: AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {}
|
||||
void start() override { this->initial_run_ = true; }
|
||||
@@ -81,7 +80,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
|
||||
|
||||
class AddressableRainbowLightEffect : public AddressableLightEffect {
|
||||
public:
|
||||
explicit AddressableRainbowLightEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
explicit AddressableRainbowLightEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
void apply(AddressableLight &it, const Color ¤t_color) override {
|
||||
ESPHSVColor hsv;
|
||||
hsv.value = 255;
|
||||
@@ -112,8 +111,8 @@ struct AddressableColorWipeEffectColor {
|
||||
|
||||
class AddressableColorWipeEffect : public AddressableLightEffect {
|
||||
public:
|
||||
explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
void set_colors(const std::vector<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; }
|
||||
explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
void set_colors(const std::initializer_list<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; }
|
||||
void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; }
|
||||
void set_reverse(bool reverse) { this->reverse_ = reverse; }
|
||||
void apply(AddressableLight &it, const Color ¤t_color) override {
|
||||
@@ -155,7 +154,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<AddressableColorWipeEffectColor> colors_;
|
||||
FixedVector<AddressableColorWipeEffectColor> colors_;
|
||||
size_t at_color_{0};
|
||||
uint32_t last_add_{0};
|
||||
uint32_t add_led_interval_{};
|
||||
@@ -165,7 +164,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
|
||||
|
||||
class AddressableScanEffect : public AddressableLightEffect {
|
||||
public:
|
||||
explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
explicit AddressableScanEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; }
|
||||
void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; }
|
||||
void apply(AddressableLight &it, const Color ¤t_color) override {
|
||||
@@ -202,7 +201,7 @@ class AddressableScanEffect : public AddressableLightEffect {
|
||||
|
||||
class AddressableTwinkleEffect : public AddressableLightEffect {
|
||||
public:
|
||||
explicit AddressableTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
explicit AddressableTwinkleEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
void apply(AddressableLight &addressable, const Color ¤t_color) override {
|
||||
const uint32_t now = millis();
|
||||
uint8_t pos_add = 0;
|
||||
@@ -244,7 +243,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect {
|
||||
|
||||
class AddressableRandomTwinkleEffect : public AddressableLightEffect {
|
||||
public:
|
||||
explicit AddressableRandomTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
explicit AddressableRandomTwinkleEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
void apply(AddressableLight &it, const Color ¤t_color) override {
|
||||
const uint32_t now = millis();
|
||||
uint8_t pos_add = 0;
|
||||
@@ -293,7 +292,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect {
|
||||
|
||||
class AddressableFireworksEffect : public AddressableLightEffect {
|
||||
public:
|
||||
explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
explicit AddressableFireworksEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
void start() override {
|
||||
auto &it = *this->get_addressable_();
|
||||
it.all() = Color::BLACK;
|
||||
@@ -342,7 +341,7 @@ class AddressableFireworksEffect : public AddressableLightEffect {
|
||||
|
||||
class AddressableFlickerEffect : public AddressableLightEffect {
|
||||
public:
|
||||
explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
explicit AddressableFlickerEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
void apply(AddressableLight &it, const Color ¤t_color) override {
|
||||
const uint32_t now = millis();
|
||||
const uint8_t intensity = this->intensity_;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "light_effect.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -17,7 +17,7 @@ inline static float random_cubic_float() {
|
||||
/// Pulse effect.
|
||||
class PulseLightEffect : public LightEffect {
|
||||
public:
|
||||
explicit PulseLightEffect(const std::string &name) : LightEffect(name) {}
|
||||
explicit PulseLightEffect(const char *name) : LightEffect(name) {}
|
||||
|
||||
void apply() override {
|
||||
const uint32_t now = millis();
|
||||
@@ -60,7 +60,7 @@ class PulseLightEffect : public LightEffect {
|
||||
/// Random effect. Sets random colors every 10 seconds and slowly transitions between them.
|
||||
class RandomLightEffect : public LightEffect {
|
||||
public:
|
||||
explicit RandomLightEffect(const std::string &name) : LightEffect(name) {}
|
||||
explicit RandomLightEffect(const char *name) : LightEffect(name) {}
|
||||
|
||||
void apply() override {
|
||||
const uint32_t now = millis();
|
||||
@@ -112,7 +112,7 @@ class RandomLightEffect : public LightEffect {
|
||||
|
||||
class LambdaLightEffect : public LightEffect {
|
||||
public:
|
||||
LambdaLightEffect(const std::string &name, std::function<void(bool initial_run)> f, uint32_t update_interval)
|
||||
LambdaLightEffect(const char *name, std::function<void(bool initial_run)> f, uint32_t update_interval)
|
||||
: LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {}
|
||||
|
||||
void start() override { this->initial_run_ = true; }
|
||||
@@ -138,7 +138,7 @@ class LambdaLightEffect : public LightEffect {
|
||||
|
||||
class AutomationLightEffect : public LightEffect {
|
||||
public:
|
||||
AutomationLightEffect(const std::string &name) : LightEffect(name) {}
|
||||
AutomationLightEffect(const char *name) : LightEffect(name) {}
|
||||
void stop() override { this->trig_->stop_action(); }
|
||||
void apply() override {
|
||||
if (!this->trig_->is_action_running()) {
|
||||
@@ -163,7 +163,7 @@ struct StrobeLightEffectColor {
|
||||
|
||||
class StrobeLightEffect : public LightEffect {
|
||||
public:
|
||||
explicit StrobeLightEffect(const std::string &name) : LightEffect(name) {}
|
||||
explicit StrobeLightEffect(const char *name) : LightEffect(name) {}
|
||||
void apply() override {
|
||||
const uint32_t now = millis();
|
||||
if (now - this->last_switch_ < this->colors_[this->at_color_].duration)
|
||||
@@ -188,17 +188,17 @@ class StrobeLightEffect : public LightEffect {
|
||||
this->last_switch_ = now;
|
||||
}
|
||||
|
||||
void set_colors(const std::vector<StrobeLightEffectColor> &colors) { this->colors_ = colors; }
|
||||
void set_colors(const std::initializer_list<StrobeLightEffectColor> &colors) { this->colors_ = colors; }
|
||||
|
||||
protected:
|
||||
std::vector<StrobeLightEffectColor> colors_;
|
||||
FixedVector<StrobeLightEffectColor> colors_;
|
||||
uint32_t last_switch_{0};
|
||||
size_t at_color_{0};
|
||||
};
|
||||
|
||||
class FlickerLightEffect : public LightEffect {
|
||||
public:
|
||||
explicit FlickerLightEffect(const std::string &name) : LightEffect(name) {}
|
||||
explicit FlickerLightEffect(const char *name) : LightEffect(name) {}
|
||||
|
||||
void apply() override {
|
||||
LightColorValues remote = this->state_->remote_values;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "esphome/core/finite_set_mask.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
@@ -107,13 +108,9 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
|
||||
// Type alias for raw color mode bitmask values
|
||||
using color_mode_bitmask_t = uint16_t;
|
||||
|
||||
// Constants for ColorMode count and bit range
|
||||
static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE
|
||||
static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type
|
||||
|
||||
// Compile-time array of all ColorMode values in declaration order
|
||||
// Bit positions (0-9) map directly to enum declaration order
|
||||
static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
|
||||
// Lookup table for ColorMode bit mapping
|
||||
// This array defines the canonical order of color modes (bit 0-9)
|
||||
constexpr ColorMode COLOR_MODE_LOOKUP[] = {
|
||||
ColorMode::UNKNOWN, // bit 0
|
||||
ColorMode::ON_OFF, // bit 1
|
||||
ColorMode::BRIGHTNESS, // bit 2
|
||||
@@ -126,33 +123,42 @@ static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
|
||||
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
|
||||
};
|
||||
|
||||
/// Map ColorMode enum values to bit positions (0-9)
|
||||
/// Bit positions follow the enum declaration order
|
||||
static constexpr int mode_to_bit(ColorMode mode) {
|
||||
// Linear search through COLOR_MODES array
|
||||
// Compiler optimizes this to efficient code since array is constexpr
|
||||
for (int i = 0; i < COLOR_MODE_COUNT; ++i) {
|
||||
if (COLOR_MODES[i] == mode)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/// Bit mapping policy for ColorMode
|
||||
/// Uses lookup table for non-contiguous enum values
|
||||
struct ColorModeBitPolicy {
|
||||
using mask_t = uint16_t; // 10 bits requires uint16_t
|
||||
static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]);
|
||||
|
||||
/// Map bit positions (0-9) to ColorMode enum values
|
||||
/// Bit positions follow the enum declaration order
|
||||
static constexpr ColorMode bit_to_mode(int bit) {
|
||||
// Direct lookup in COLOR_MODES array
|
||||
return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN;
|
||||
}
|
||||
static constexpr unsigned to_bit(ColorMode mode) {
|
||||
// Linear search through lookup table
|
||||
// Compiler optimizes this to efficient code since array is constexpr
|
||||
for (int i = 0; i < MAX_BITS; ++i) {
|
||||
if (COLOR_MODE_LOOKUP[i] == mode)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static constexpr ColorMode from_bit(unsigned bit) {
|
||||
return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
// Type alias for ColorMode bitmask using policy-based design
|
||||
using ColorModeMask = FiniteSetMask<ColorMode, ColorModeBitPolicy>;
|
||||
|
||||
// Number of ColorCapability enum values
|
||||
constexpr int COLOR_CAPABILITY_COUNT = 6;
|
||||
|
||||
/// Helper to compute capability bitmask at compile time
|
||||
static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) {
|
||||
color_mode_bitmask_t mask = 0;
|
||||
constexpr uint16_t compute_capability_bitmask(ColorCapability capability) {
|
||||
uint16_t mask = 0;
|
||||
uint8_t cap_bit = static_cast<uint8_t>(capability);
|
||||
|
||||
// Check each ColorMode to see if it has this capability
|
||||
for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) {
|
||||
uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit));
|
||||
constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]);
|
||||
for (int bit = 0; bit < color_mode_count; ++bit) {
|
||||
uint8_t mode_val = static_cast<uint8_t>(COLOR_MODE_LOOKUP[bit]);
|
||||
if ((mode_val & cap_bit) != 0) {
|
||||
mask |= (1 << bit);
|
||||
}
|
||||
@@ -160,12 +166,9 @@ static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability
|
||||
return mask;
|
||||
}
|
||||
|
||||
// Number of ColorCapability enum values
|
||||
static constexpr int COLOR_CAPABILITY_COUNT = 6;
|
||||
|
||||
/// Compile-time lookup table mapping ColorCapability to bitmask
|
||||
/// This array is computed at compile time using constexpr
|
||||
static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
|
||||
constexpr uint16_t CAPABILITY_BITMASKS[] = {
|
||||
compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0
|
||||
compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1
|
||||
compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2
|
||||
@@ -174,130 +177,38 @@ static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
|
||||
compute_capability_bitmask(ColorCapability::RGB), // 1 << 5
|
||||
};
|
||||
|
||||
/// Bitmask for storing a set of ColorMode values efficiently.
|
||||
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
|
||||
class ColorModeMask {
|
||||
public:
|
||||
constexpr ColorModeMask() = default;
|
||||
|
||||
/// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE}
|
||||
constexpr ColorModeMask(std::initializer_list<ColorMode> modes) {
|
||||
for (auto mode : modes) {
|
||||
this->add(mode);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
|
||||
|
||||
/// Add multiple modes at once using initializer list
|
||||
constexpr void add(std::initializer_list<ColorMode> modes) {
|
||||
for (auto mode : modes) {
|
||||
this->add(mode);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
|
||||
|
||||
constexpr size_t size() const {
|
||||
// Count set bits using Brian Kernighan's algorithm
|
||||
// More efficient for sparse bitmasks (typical case: 2-4 modes out of 10)
|
||||
uint16_t n = this->mask_;
|
||||
size_t count = 0;
|
||||
while (n) {
|
||||
n &= n - 1; // Clear the least significant set bit
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
constexpr bool empty() const { return this->mask_ == 0; }
|
||||
|
||||
/// Iterator support for API encoding
|
||||
class Iterator {
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using value_type = ColorMode;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using pointer = const ColorMode *;
|
||||
using reference = ColorMode;
|
||||
|
||||
constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
|
||||
|
||||
constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
|
||||
|
||||
constexpr Iterator &operator++() {
|
||||
++bit_;
|
||||
advance_to_next_set_bit_();
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
|
||||
|
||||
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
|
||||
|
||||
private:
|
||||
constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); }
|
||||
|
||||
color_mode_bitmask_t mask_;
|
||||
int bit_;
|
||||
};
|
||||
|
||||
constexpr Iterator begin() const { return Iterator(mask_, 0); }
|
||||
constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); }
|
||||
|
||||
/// Get the raw bitmask value for API encoding
|
||||
constexpr color_mode_bitmask_t get_mask() const { return this->mask_; }
|
||||
|
||||
/// Find the next set bit in a bitmask starting from a given position
|
||||
/// Returns the bit position, or MAX_BIT_INDEX if no more bits are set
|
||||
static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) {
|
||||
int bit = start_bit;
|
||||
while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) {
|
||||
++bit;
|
||||
}
|
||||
return bit;
|
||||
}
|
||||
|
||||
/// Find the first set bit in a bitmask and return the corresponding ColorMode
|
||||
/// Used for optimizing compute_color_mode_() intersection logic
|
||||
static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) {
|
||||
return bit_to_mode(find_next_set_bit(mask, 0));
|
||||
}
|
||||
|
||||
/// Check if a ColorMode is present in a raw bitmask value
|
||||
/// Useful for checking intersection results without creating a temporary ColorModeMask
|
||||
static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) {
|
||||
return (mask & (1 << mode_to_bit(mode))) != 0;
|
||||
}
|
||||
|
||||
/// Check if any mode in the bitmask has a specific capability
|
||||
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
|
||||
bool has_capability(ColorCapability capability) const {
|
||||
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
|
||||
// ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
|
||||
// We need to convert the power-of-2 value to an index
|
||||
uint8_t cap_val = static_cast<uint8_t>(capability);
|
||||
/**
|
||||
* @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS
|
||||
* lookup.
|
||||
*
|
||||
* This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5).
|
||||
* Used to index into the CAPABILITY_BITMASKS lookup table.
|
||||
*
|
||||
* @param capability A ColorCapability enum value (must be a power of 2).
|
||||
* @return The corresponding array index (0-based).
|
||||
*/
|
||||
inline int capability_to_index(ColorCapability capability) {
|
||||
uint8_t cap_val = static_cast<uint8_t>(capability);
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
|
||||
int index = __builtin_ctz(cap_val);
|
||||
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
|
||||
return __builtin_ctz(cap_val);
|
||||
#else
|
||||
// Fallback for compilers without __builtin_ctz
|
||||
int index = 0;
|
||||
while (cap_val > 1) {
|
||||
cap_val >>= 1;
|
||||
++index;
|
||||
}
|
||||
#endif
|
||||
return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0;
|
||||
// Fallback for compilers without __builtin_ctz
|
||||
int index = 0;
|
||||
while (cap_val > 1) {
|
||||
cap_val >>= 1;
|
||||
++index;
|
||||
}
|
||||
return index;
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
// Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan).
|
||||
// Currently only 10 ColorMode values exist, so 16 bits is sufficient.
|
||||
// Can be changed to uint32_t if more than 16 color modes are needed in the future.
|
||||
// Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes).
|
||||
color_mode_bitmask_t mask_{0};
|
||||
};
|
||||
/// Check if any mode in the bitmask has a specific capability
|
||||
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
|
||||
inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) {
|
||||
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
|
||||
return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0;
|
||||
}
|
||||
|
||||
} // namespace light
|
||||
} // namespace esphome
|
||||
|
||||
@@ -156,7 +156,7 @@ void LightCall::perform() {
|
||||
if (this->effect_ == 0u) {
|
||||
effect_s = "None";
|
||||
} else {
|
||||
effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str();
|
||||
effect_s = this->parent_->effects_[this->effect_ - 1]->get_name();
|
||||
}
|
||||
|
||||
if (publish) {
|
||||
@@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() {
|
||||
|
||||
// Use the preferred suitable mode.
|
||||
if (intersection != 0) {
|
||||
ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
|
||||
ColorMode mode = ColorModeMask::first_value_from_mask(intersection);
|
||||
ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
|
||||
LOG_STR_ARG(color_mode_to_human(mode)));
|
||||
return mode;
|
||||
@@ -511,7 +511,7 @@ LightCall &LightCall::set_effect(const std::string &effect) {
|
||||
for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) {
|
||||
LightEffect *e = this->parent_->effects_[i];
|
||||
|
||||
if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) {
|
||||
if (strcasecmp(effect.c_str(), e->get_name()) == 0) {
|
||||
this->set_effect(i + 1);
|
||||
found = true;
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -11,7 +9,7 @@ class LightState;
|
||||
|
||||
class LightEffect {
|
||||
public:
|
||||
explicit LightEffect(std::string name) : name_(std::move(name)) {}
|
||||
explicit LightEffect(const char *name) : name_(name) {}
|
||||
|
||||
/// Initialize this LightEffect. Will be called once after creation.
|
||||
virtual void start() {}
|
||||
@@ -24,7 +22,11 @@ class LightEffect {
|
||||
/// Apply this effect. Use the provided state for starting transitions, ...
|
||||
virtual void apply() = 0;
|
||||
|
||||
const std::string &get_name() { return this->name_; }
|
||||
/**
|
||||
* Returns the name of this effect.
|
||||
* The returned pointer is valid for the lifetime of the program and must not be freed.
|
||||
*/
|
||||
const char *get_name() const { return this->name_; }
|
||||
|
||||
/// Internal method called by the LightState when this light effect is registered in it.
|
||||
virtual void init() {}
|
||||
@@ -47,7 +49,7 @@ class LightEffect {
|
||||
|
||||
protected:
|
||||
LightState *state_{nullptr};
|
||||
std::string name_;
|
||||
const char *name_;
|
||||
|
||||
/// Internal method to find this effect's index in the parent light's effect list.
|
||||
uint32_t get_index_in_parent_() const;
|
||||
|
||||
@@ -178,12 +178,9 @@ void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore
|
||||
void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; }
|
||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
|
||||
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||
void LightState::add_effects(const std::vector<LightEffect *> &effects) {
|
||||
void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) {
|
||||
// Called once from Python codegen during setup with all effects from YAML config
|
||||
this->effects_.init(effects.size());
|
||||
for (auto *effect : effects) {
|
||||
this->effects_.push_back(effect);
|
||||
}
|
||||
this->effects_ = effects;
|
||||
}
|
||||
|
||||
void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
|
||||
|
||||
@@ -163,7 +163,7 @@ class LightState : public EntityBase, public Component {
|
||||
const FixedVector<LightEffect *> &get_effects() const;
|
||||
|
||||
/// Add effects for this light state.
|
||||
void add_effects(const std::vector<LightEffect *> &effects);
|
||||
void add_effects(const std::initializer_list<LightEffect *> &effects);
|
||||
|
||||
/// Get the total number of effects available for this light.
|
||||
size_t get_effect_count() const { return this->effects_.size(); }
|
||||
@@ -177,7 +177,7 @@ class LightState : public EntityBase, public Component {
|
||||
return 0;
|
||||
}
|
||||
for (size_t i = 0; i < this->effects_.size(); i++) {
|
||||
if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name().c_str()) == 0) {
|
||||
if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name()) == 0) {
|
||||
return i + 1; // Effects are 1-indexed in active_effect_index_
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ class LightTraits {
|
||||
this->supported_color_modes_ = ColorModeMask(modes);
|
||||
}
|
||||
|
||||
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
|
||||
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; }
|
||||
bool supports_color_capability(ColorCapability color_capability) const {
|
||||
return this->supported_color_modes_.has_capability(color_capability);
|
||||
return has_capability(this->supported_color_modes_, color_capability);
|
||||
}
|
||||
|
||||
float get_min_mireds() const { return this->min_mireds_; }
|
||||
|
||||
@@ -99,7 +99,11 @@ const std::string &get_use_address() {
|
||||
return wifi::global_wifi_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI)
|
||||
#ifdef USE_OPENTHREAD
|
||||
return openthread::global_openthread_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD)
|
||||
// Fallback when no network component is defined (e.g., host platform)
|
||||
static const std::string empty;
|
||||
return empty;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
@@ -277,3 +278,19 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
|
||||
raise EsphomeError(f"Upload failed with result: {result}")
|
||||
|
||||
return handled
|
||||
|
||||
|
||||
def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
|
||||
address = devices[0]
|
||||
from .ble_logger import is_mac_address, logger_connect, logger_scan
|
||||
|
||||
if devices[0] == "BLE":
|
||||
ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"]))
|
||||
if ble_device:
|
||||
address = ble_device.address
|
||||
else:
|
||||
return True
|
||||
if is_mac_address(address):
|
||||
asyncio.run(logger_connect(address))
|
||||
return True
|
||||
return False
|
||||
|
||||
60
esphome/components/nrf52/ble_logger.py
Normal file
60
esphome/components/nrf52/ble_logger.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from typing import Final
|
||||
|
||||
from bleak import BleakClient, BleakScanner, BLEDevice
|
||||
from bleak.exc import (
|
||||
BleakCharacteristicNotFoundError,
|
||||
BleakDBusError,
|
||||
BleakDeviceNotFoundError,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
|
||||
MAC_ADDRESS_PATTERN: Final = re.compile(
|
||||
r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def is_mac_address(value: str) -> bool:
|
||||
return MAC_ADDRESS_PATTERN.match(value)
|
||||
|
||||
|
||||
async def logger_scan(name: str) -> BLEDevice | None:
|
||||
_LOGGER.info("Scanning bluetooth for %s...", name)
|
||||
device = await BleakScanner.find_device_by_name(name)
|
||||
if not device:
|
||||
_LOGGER.error("%s Bluetooth LE device was not found!", name)
|
||||
return device
|
||||
|
||||
|
||||
async def logger_connect(host: str) -> int | None:
|
||||
disconnected_event = asyncio.Event()
|
||||
|
||||
def handle_disconnect(client):
|
||||
disconnected_event.set()
|
||||
|
||||
def handle_rx(_, data: bytearray):
|
||||
print(data.decode("utf-8"), end="")
|
||||
|
||||
_LOGGER.info("Connecting %s...", host)
|
||||
try:
|
||||
async with BleakClient(host, disconnected_callback=handle_disconnect) as client:
|
||||
_LOGGER.info("Connected %s...", host)
|
||||
try:
|
||||
await client.start_notify(NUS_TX_CHAR_UUID, handle_rx)
|
||||
except BleakDBusError as e:
|
||||
_LOGGER.error("Bluetooth LE logger: %s", e)
|
||||
disconnected_event.set()
|
||||
await disconnected_event.wait()
|
||||
except BleakDeviceNotFoundError:
|
||||
_LOGGER.error("Device %s not found", host)
|
||||
return 1
|
||||
except BleakCharacteristicNotFoundError:
|
||||
_LOGGER.error("Device %s has no NUS characteristic", host)
|
||||
return 1
|
||||
@@ -8,8 +8,10 @@ from esphome.components.esp32 import (
|
||||
)
|
||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID
|
||||
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_TYPE,
|
||||
@@ -108,6 +110,12 @@ _CONNECTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _validate(config: ConfigType) -> ConfigType:
|
||||
if CONF_USE_ADDRESS not in config:
|
||||
config[CONF_USE_ADDRESS] = f"{CORE.name}.local"
|
||||
return config
|
||||
|
||||
|
||||
def _require_vfs_select(config):
|
||||
"""Register VFS select requirement during config validation."""
|
||||
# OpenThread uses esp_vfs_eventfd which requires VFS select support
|
||||
@@ -126,11 +134,13 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_FORCE_DATASET): cv.boolean,
|
||||
cv.Optional(CONF_TLV): cv.string_strict,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
}
|
||||
).extend(_CONNECTION_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
|
||||
cv.only_with_esp_idf,
|
||||
only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]),
|
||||
_validate,
|
||||
_require_vfs_select,
|
||||
)
|
||||
|
||||
@@ -155,6 +165,7 @@ async def to_code(config):
|
||||
enable_mdns_storage()
|
||||
|
||||
ot = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(ot.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
await cg.register_component(ot, config)
|
||||
|
||||
srp = cg.new_Pvariable(config[CONF_SRP_ID])
|
||||
|
||||
@@ -252,6 +252,12 @@ void OpenThreadComponent::on_factory_reset(std::function<void()> callback) {
|
||||
ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services");
|
||||
}
|
||||
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; }
|
||||
|
||||
void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
|
||||
|
||||
} // namespace openthread
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -33,11 +33,15 @@ class OpenThreadComponent : public Component {
|
||||
void on_factory_reset(std::function<void()> callback);
|
||||
void defer_factory_reset_external_callback();
|
||||
|
||||
const std::string &get_use_address() const;
|
||||
void set_use_address(const std::string &use_address);
|
||||
|
||||
protected:
|
||||
std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
|
||||
bool teardown_started_{false};
|
||||
bool teardown_complete_{false};
|
||||
std::function<void()> factory_reset_external_callback_;
|
||||
std::string use_address_;
|
||||
};
|
||||
|
||||
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -213,11 +213,15 @@ def _validate(config):
|
||||
if CONF_EAP in config:
|
||||
network[CONF_EAP] = config.pop(CONF_EAP)
|
||||
if CONF_NETWORKS in config:
|
||||
raise cv.Invalid(
|
||||
"You cannot use the 'ssid:' option together with 'networks:'. Please "
|
||||
"copy your network into the 'networks:' key"
|
||||
)
|
||||
config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
|
||||
# In testing mode, merged component tests may have both ssid and networks
|
||||
# Just use the networks list and ignore the single ssid
|
||||
if not CORE.testing_mode:
|
||||
raise cv.Invalid(
|
||||
"You cannot use the 'ssid:' option together with 'networks:'. Please "
|
||||
"copy your network into the 'networks:' key"
|
||||
)
|
||||
else:
|
||||
config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
|
||||
|
||||
if (CONF_NETWORKS not in config) and (CONF_AP not in config):
|
||||
config = config.copy()
|
||||
@@ -378,14 +382,19 @@ async def to_code(config):
|
||||
# Track if any network uses Enterprise authentication
|
||||
has_eap = False
|
||||
|
||||
def add_sta(ap, network):
|
||||
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
|
||||
cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
|
||||
# Initialize FixedVector with the count of networks
|
||||
networks = config.get(CONF_NETWORKS, [])
|
||||
if networks:
|
||||
cg.add(var.init_sta(len(networks)))
|
||||
|
||||
for network in config.get(CONF_NETWORKS, []):
|
||||
if CONF_EAP in network:
|
||||
has_eap = True
|
||||
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
|
||||
def add_sta(ap: cg.MockObj, network: dict) -> None:
|
||||
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
|
||||
cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
|
||||
|
||||
for network in networks:
|
||||
if CONF_EAP in network:
|
||||
has_eap = True
|
||||
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
|
||||
|
||||
if CONF_AP in config:
|
||||
conf = config[CONF_AP]
|
||||
|
||||
@@ -330,9 +330,11 @@ float WiFiComponent::get_loop_priority() const {
|
||||
return 10.0f; // before other loop components
|
||||
}
|
||||
|
||||
void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); }
|
||||
void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
|
||||
void WiFiComponent::set_sta(const WiFiAP &ap) {
|
||||
this->clear_sta();
|
||||
this->init_sta(1);
|
||||
this->add_sta(ap);
|
||||
}
|
||||
void WiFiComponent::clear_sta() { this->sta_.clear(); }
|
||||
|
||||
@@ -219,6 +219,7 @@ class WiFiComponent : public Component {
|
||||
|
||||
void set_sta(const WiFiAP &ap);
|
||||
WiFiAP get_sta() { return this->selected_ap_; }
|
||||
void init_sta(size_t count);
|
||||
void add_sta(const WiFiAP &ap);
|
||||
void clear_sta();
|
||||
|
||||
@@ -393,7 +394,7 @@ class WiFiComponent : public Component {
|
||||
#endif
|
||||
|
||||
std::string use_address_;
|
||||
std::vector<WiFiAP> sta_;
|
||||
FixedVector<WiFiAP> sta_;
|
||||
std::vector<WiFiSTAPriority> sta_priorities_;
|
||||
wifi_scan_vector_t<WiFiScanResult> scan_result_;
|
||||
WiFiAP selected_ap_;
|
||||
|
||||
@@ -28,7 +28,7 @@ const int DEFAULT_BLANK_TIME = 1000;
|
||||
|
||||
static const char *const TAG = "wled_light_effect";
|
||||
|
||||
WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {}
|
||||
WLEDLightEffect::WLEDLightEffect(const char *name) : AddressableLightEffect(name) {}
|
||||
|
||||
void WLEDLightEffect::start() {
|
||||
AddressableLightEffect::start();
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace wled {
|
||||
|
||||
class WLEDLightEffect : public light::AddressableLightEffect {
|
||||
public:
|
||||
WLEDLightEffect(const std::string &name);
|
||||
WLEDLightEffect(const char *name);
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
|
||||
@@ -234,6 +234,9 @@ def copy_files():
|
||||
"url": "https://esphome.io/",
|
||||
"vendor": "esphome",
|
||||
"build": {
|
||||
"bsp": {
|
||||
"name": "adafruit"
|
||||
},
|
||||
"softdevice": {
|
||||
"sd_fwid": "0x00B6"
|
||||
}
|
||||
|
||||
@@ -636,11 +636,9 @@ class EsphomeCore:
|
||||
if self.config is None:
|
||||
raise ValueError("Config has not been loaded yet")
|
||||
|
||||
if CONF_WIFI in self.config:
|
||||
return self.config[CONF_WIFI][CONF_USE_ADDRESS]
|
||||
|
||||
if CONF_ETHERNET in self.config:
|
||||
return self.config[CONF_ETHERNET][CONF_USE_ADDRESS]
|
||||
for network_type in (CONF_WIFI, CONF_ETHERNET, CONF_OPENTHREAD):
|
||||
if network_type in self.config:
|
||||
return self.config[network_type][CONF_USE_ADDRESS]
|
||||
|
||||
if CONF_OPENTHREAD in self.config:
|
||||
return f"{self.name}.local"
|
||||
|
||||
@@ -243,8 +243,10 @@
|
||||
// Dummy firmware payload for shelly_dimmer
|
||||
#define USE_SHD_FIRMWARE_MAJOR_VERSION 56
|
||||
#define USE_SHD_FIRMWARE_MINOR_VERSION 5
|
||||
// clang-format off
|
||||
#define USE_SHD_FIRMWARE_DATA \
|
||||
{}
|
||||
// clang-format on
|
||||
|
||||
#define USE_WEBSERVER
|
||||
#define USE_WEBSERVER_AUTH
|
||||
|
||||
171
esphome/core/finite_set_mask.h
Normal file
171
esphome/core/finite_set_mask.h
Normal file
@@ -0,0 +1,171 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <type_traits>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Default bit mapping policy for contiguous enums starting at 0
|
||||
/// Provides 1:1 mapping where enum value equals bit position
|
||||
template<typename ValueType, int MaxBits> struct DefaultBitPolicy {
|
||||
// Automatic bitmask type selection based on MaxBits
|
||||
// ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t
|
||||
using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t,
|
||||
typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type;
|
||||
|
||||
static constexpr int MAX_BITS = MaxBits;
|
||||
|
||||
static constexpr unsigned to_bit(ValueType value) { return static_cast<unsigned>(value); }
|
||||
|
||||
static constexpr ValueType from_bit(unsigned bit) { return static_cast<ValueType>(bit); }
|
||||
};
|
||||
|
||||
/// Generic bitmask for storing a finite set of discrete values efficiently.
|
||||
/// Replaces std::set<ValueType> to eliminate red-black tree overhead (~586 bytes per instantiation).
|
||||
///
|
||||
/// Template parameters:
|
||||
/// ValueType: The type to store (typically enum, but can be any discrete bounded type)
|
||||
/// BitPolicy: Policy class defining bit mapping and mask type (defaults to DefaultBitPolicy)
|
||||
///
|
||||
/// BitPolicy requirements:
|
||||
/// - using mask_t = <uint8_t|uint16_t|uint32_t> // Bitmask storage type
|
||||
/// - static constexpr int MAX_BITS // Maximum number of bits
|
||||
/// - static constexpr unsigned to_bit(ValueType) // Convert value to bit position
|
||||
/// - static constexpr ValueType from_bit(unsigned) // Convert bit position to value
|
||||
///
|
||||
/// Example usage (1:1 mapping - climate enums):
|
||||
/// // For contiguous enums starting at 0, use DefaultBitPolicy
|
||||
/// using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
|
||||
/// ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
|
||||
/// if (modes.count(CLIMATE_MODE_HEAT)) { ... }
|
||||
/// for (auto mode : modes) { ... }
|
||||
///
|
||||
/// Example usage (custom mapping - ColorMode):
|
||||
/// // For custom mappings, define a custom BitPolicy
|
||||
/// // See esphome/components/light/color_mode.h for complete example
|
||||
///
|
||||
/// Design notes:
|
||||
/// - Policy-based design allows custom bit mappings without template specialization
|
||||
/// - Iterator converts bit positions to actual values during traversal
|
||||
/// - All operations are constexpr-compatible for compile-time initialization
|
||||
/// - Drop-in replacement for std::set<ValueType> with simpler API
|
||||
///
|
||||
template<typename ValueType, typename BitPolicy = DefaultBitPolicy<ValueType, 16>> class FiniteSetMask {
|
||||
public:
|
||||
using bitmask_t = typename BitPolicy::mask_t;
|
||||
|
||||
constexpr FiniteSetMask() = default;
|
||||
|
||||
/// Construct from initializer list: {VALUE1, VALUE2, ...}
|
||||
constexpr FiniteSetMask(std::initializer_list<ValueType> values) {
|
||||
for (auto value : values) {
|
||||
this->insert(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a single value to the set (std::set compatibility)
|
||||
constexpr void insert(ValueType value) { this->mask_ |= (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value)); }
|
||||
|
||||
/// Add multiple values from initializer list
|
||||
constexpr void insert(std::initializer_list<ValueType> values) {
|
||||
for (auto value : values) {
|
||||
this->insert(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a value from the set (std::set compatibility)
|
||||
constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << BitPolicy::to_bit(value)); }
|
||||
|
||||
/// Clear all values from the set
|
||||
constexpr void clear() { this->mask_ = 0; }
|
||||
|
||||
/// Check if the set contains a specific value (std::set compatibility)
|
||||
/// Returns 1 if present, 0 if not (same as std::set for unique elements)
|
||||
constexpr size_t count(ValueType value) const {
|
||||
return (this->mask_ & (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value))) != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/// Count the number of values in the set
|
||||
constexpr size_t size() const {
|
||||
// Brian Kernighan's algorithm - efficient for sparse bitmasks
|
||||
// Typical case: 2-4 modes out of 10 possible
|
||||
bitmask_t n = this->mask_;
|
||||
size_t count = 0;
|
||||
while (n) {
|
||||
n &= n - 1; // Clear the least significant set bit
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Check if the set is empty
|
||||
constexpr bool empty() const { return this->mask_ == 0; }
|
||||
|
||||
/// Iterator support for range-based for loops and API encoding
|
||||
/// Iterates over set bits and converts bit positions to values
|
||||
/// Optimization: removes bits from mask as we iterate
|
||||
class Iterator {
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using value_type = ValueType;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using pointer = const ValueType *;
|
||||
using reference = ValueType;
|
||||
|
||||
constexpr explicit Iterator(bitmask_t mask) : mask_(mask) {}
|
||||
|
||||
constexpr ValueType operator*() const {
|
||||
// Return value for the first set bit
|
||||
return BitPolicy::from_bit(find_next_set_bit(mask_, 0));
|
||||
}
|
||||
|
||||
constexpr Iterator &operator++() {
|
||||
// Clear the lowest set bit (Brian Kernighan's algorithm)
|
||||
mask_ &= mask_ - 1;
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr bool operator==(const Iterator &other) const { return mask_ == other.mask_; }
|
||||
|
||||
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
|
||||
|
||||
private:
|
||||
bitmask_t mask_;
|
||||
};
|
||||
|
||||
constexpr Iterator begin() const { return Iterator(mask_); }
|
||||
constexpr Iterator end() const { return Iterator(0); }
|
||||
|
||||
/// Get the raw bitmask value for optimized operations
|
||||
constexpr bitmask_t get_mask() const { return this->mask_; }
|
||||
|
||||
/// Check if a specific value is present in a raw bitmask
|
||||
/// Useful for checking intersection results without creating temporary objects
|
||||
static constexpr bool mask_contains(bitmask_t mask, ValueType value) {
|
||||
return (mask & (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value))) != 0;
|
||||
}
|
||||
|
||||
/// Get the first value from a raw bitmask
|
||||
/// Used for optimizing intersection logic (e.g., "pick first suitable mode")
|
||||
static constexpr ValueType first_value_from_mask(bitmask_t mask) {
|
||||
return BitPolicy::from_bit(find_next_set_bit(mask, 0));
|
||||
}
|
||||
|
||||
/// Find the next set bit in a bitmask starting from a given position
|
||||
/// Returns the bit position, or MAX_BITS if no more bits are set
|
||||
static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) {
|
||||
int bit = start_bit;
|
||||
while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast<bitmask_t>(1) << bit))) {
|
||||
++bit;
|
||||
}
|
||||
return bit;
|
||||
}
|
||||
|
||||
protected:
|
||||
bitmask_t mask_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
@@ -194,12 +194,8 @@ template<typename T> class FixedVector {
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
public:
|
||||
FixedVector() = default;
|
||||
|
||||
/// Constructor from initializer list - allocates exact size needed
|
||||
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
|
||||
FixedVector(std::initializer_list<T> init_list) {
|
||||
// Helper to assign from initializer list (shared by constructor and assignment operator)
|
||||
void assign_from_initializer_list_(std::initializer_list<T> init_list) {
|
||||
init(init_list.size());
|
||||
size_t idx = 0;
|
||||
for (const auto &item : init_list) {
|
||||
@@ -209,6 +205,13 @@ template<typename T> class FixedVector {
|
||||
size_ = init_list.size();
|
||||
}
|
||||
|
||||
public:
|
||||
FixedVector() = default;
|
||||
|
||||
/// Constructor from initializer list - allocates exact size needed
|
||||
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
|
||||
FixedVector(std::initializer_list<T> init_list) { assign_from_initializer_list_(init_list); }
|
||||
|
||||
~FixedVector() { cleanup_(); }
|
||||
|
||||
// Disable copy operations (avoid accidental expensive copies)
|
||||
@@ -234,6 +237,15 @@ template<typename T> class FixedVector {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Assignment from initializer list - avoids temporary and move overhead
|
||||
/// This enables: FixedVector<int> v; v = {1, 2, 3};
|
||||
FixedVector &operator=(std::initializer_list<T> init_list) {
|
||||
cleanup_();
|
||||
reset_();
|
||||
assign_from_initializer_list_(init_list);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Allocate capacity - can be called multiple times to reinit
|
||||
void init(size_t n) {
|
||||
cleanup_();
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
#include <atomic>
|
||||
#endif
|
||||
|
||||
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==42.2.0
|
||||
aioesphomeapi==42.3.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.15 # dashboard_import
|
||||
@@ -22,6 +22,7 @@ pillow==11.3.0
|
||||
cairosvg==2.8.2
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==1.1.1
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.0
|
||||
|
||||
@@ -336,7 +336,7 @@ def _component_has_tests(component: str) -> bool:
|
||||
Returns:
|
||||
True if the component has test YAML files
|
||||
"""
|
||||
return bool(get_component_test_files(component))
|
||||
return bool(get_component_test_files(component, all_variants=True))
|
||||
|
||||
|
||||
def _select_platform_by_preference(
|
||||
@@ -496,7 +496,7 @@ def detect_memory_impact_config(
|
||||
|
||||
for component in sorted(changed_component_set):
|
||||
# Look for test files on preferred platforms
|
||||
test_files = get_component_test_files(component)
|
||||
test_files = get_component_test_files(component, all_variants=True)
|
||||
if not test_files:
|
||||
continue
|
||||
|
||||
@@ -606,21 +606,23 @@ def main() -> None:
|
||||
# [list]: Changed components (already includes dependencies)
|
||||
changed_components_result = get_changed_components()
|
||||
|
||||
# Always analyze component files, even if core files changed
|
||||
# This is needed for component testing and memory impact analysis
|
||||
changed = changed_files(args.branch)
|
||||
component_files = [f for f in changed if filter_component_and_test_files(f)]
|
||||
|
||||
directly_changed_components = get_components_with_dependencies(
|
||||
component_files, False
|
||||
)
|
||||
|
||||
if changed_components_result is None:
|
||||
# Core files changed - will trigger full clang-tidy scan
|
||||
# No specific components to test
|
||||
changed_components = []
|
||||
directly_changed_components = []
|
||||
# But we still need to track changed components for testing and memory analysis
|
||||
changed_components = get_components_with_dependencies(component_files, True)
|
||||
is_core_change = True
|
||||
else:
|
||||
# Get both directly changed and all changed (with dependencies)
|
||||
changed = changed_files(args.branch)
|
||||
component_files = [f for f in changed if filter_component_and_test_files(f)]
|
||||
|
||||
directly_changed_components = get_components_with_dependencies(
|
||||
component_files, False
|
||||
)
|
||||
changed_components = get_components_with_dependencies(component_files, True)
|
||||
# Use the result from get_changed_components() which includes dependencies
|
||||
changed_components = changed_components_result
|
||||
is_core_change = False
|
||||
|
||||
# Filter to only components that have test files
|
||||
|
||||
@@ -49,9 +49,9 @@ def has_test_files(component_name: str, tests_dir: Path) -> bool:
|
||||
tests_dir: Path to tests/components directory (unused, kept for compatibility)
|
||||
|
||||
Returns:
|
||||
True if the component has test.*.yaml files
|
||||
True if the component has test.*.yaml or test-*.yaml files
|
||||
"""
|
||||
return bool(get_component_test_files(component_name))
|
||||
return bool(get_component_test_files(component_name, all_variants=True))
|
||||
|
||||
|
||||
def create_intelligent_batches(
|
||||
@@ -118,8 +118,13 @@ def create_intelligent_batches(
|
||||
continue
|
||||
|
||||
# Get signature from any platform (they should all have the same buses)
|
||||
# Components not in component_buses were filtered out by has_test_files check
|
||||
comp_platforms = component_buses[component]
|
||||
# Components not in component_buses may only have variant-specific tests
|
||||
comp_platforms = component_buses.get(component)
|
||||
if not comp_platforms:
|
||||
# Component has tests but no analyzable base config - treat as no buses
|
||||
signature_groups[(ALL_PLATFORMS, NO_BUSES_SIGNATURE)].append(component)
|
||||
continue
|
||||
|
||||
for platform, buses in comp_platforms.items():
|
||||
if buses:
|
||||
signature = create_grouping_signature({platform: buses}, platform)
|
||||
|
||||
@@ -6,14 +6,14 @@ sensor:
|
||||
- platform: template
|
||||
id: template_humidity
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return 0.6;
|
||||
}
|
||||
return 0.0;
|
||||
- platform: template
|
||||
id: template_temperature
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return 42.0;
|
||||
}
|
||||
return 0.0;
|
||||
|
||||
@@ -3,7 +3,7 @@ sensor:
|
||||
id: template_sensor
|
||||
name: Template Sensor
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return 42.0;
|
||||
}
|
||||
return 0.0;
|
||||
|
||||
@@ -10,7 +10,7 @@ sensor:
|
||||
- platform: template
|
||||
id: template_sensor1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return 42.0;
|
||||
} else {
|
||||
return 0.0;
|
||||
|
||||
@@ -2,21 +2,21 @@ binary_sensor:
|
||||
- platform: template
|
||||
id: bin1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
- platform: template
|
||||
id: bin2
|
||||
lambda: |-
|
||||
if (millis() > 20000) {
|
||||
if (esphome::millis() > 20000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
- platform: template
|
||||
id: bin3
|
||||
lambda: |-
|
||||
if (millis() > 30000) {
|
||||
if (esphome::millis() > 30000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -2,14 +2,14 @@ sensor:
|
||||
- platform: template
|
||||
id: template_temperature1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return 0.6;
|
||||
}
|
||||
return 0.0;
|
||||
- platform: template
|
||||
id: template_temperature2
|
||||
lambda: |-
|
||||
if (millis() > 20000) {
|
||||
if (esphome::millis() > 20000) {
|
||||
return 0.8;
|
||||
}
|
||||
return 0.0;
|
||||
|
||||
@@ -2,7 +2,7 @@ binary_sensor:
|
||||
- platform: template
|
||||
id: bin1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,7 @@ binary_sensor:
|
||||
- platform: template
|
||||
id: bin1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: DP83848
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: IP101
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: JL1101
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: KSZ8081
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: KSZ8081RNA
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: LAN8670
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: LAN8720
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: RTL8201
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ethernet:
|
||||
type: LAN8720
|
||||
mdc_pin: 23
|
||||
mdio_pin: 25
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 26
|
||||
power_pin: 33
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
|
||||
11
tests/components/fan/common.yaml
Normal file
11
tests/components/fan/common.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
fan:
|
||||
- platform: template
|
||||
id: test_fan
|
||||
name: "Test Fan"
|
||||
preset_modes:
|
||||
- Eco
|
||||
- Sleep
|
||||
- Turbo
|
||||
has_oscillating: true
|
||||
has_direction: true
|
||||
speed_count: 3
|
||||
1
tests/components/fan/test.esp8266-ard.yaml
Normal file
1
tests/components/fan/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
7
tests/components/hdc2010/common.yaml
Normal file
7
tests/components/hdc2010/common.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
sensor:
|
||||
- platform: hdc2010
|
||||
i2c_id: i2c_bus
|
||||
temperature:
|
||||
name: Temperature
|
||||
humidity:
|
||||
name: Humidity
|
||||
4
tests/components/hdc2010/test.esp32-c3-idf.yaml
Normal file
4
tests/components/hdc2010/test.esp32-c3-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/hdc2010/test.esp32-idf.yaml
Normal file
4
tests/components/hdc2010/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/hdc2010/test.esp8266-ard.yaml
Normal file
4
tests/components/hdc2010/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/hdc2010/test.rp2040-ard.yaml
Normal file
4
tests/components/hdc2010/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
8
tests/components/improv_base/common-uart0.yaml
Normal file
8
tests/components/improv_base/common-uart0.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
logger:
|
||||
hardware_uart: UART0
|
||||
|
||||
improv_serial:
|
||||
1
tests/components/improv_base/test-uart0.esp8266-ard.yaml
Normal file
1
tests/components/improv_base/test-uart0.esp8266-ard.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common-uart0.yaml
|
||||
@@ -17,6 +17,20 @@ esphome:
|
||||
relative_brightness: 5%
|
||||
brightness_limits:
|
||||
max_brightness: 90%
|
||||
- light.turn_on:
|
||||
id: test_addressable_transition
|
||||
brightness: 50%
|
||||
red: 100%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
transition_length: 500ms
|
||||
- light.turn_on:
|
||||
id: test_addressable_transition
|
||||
brightness: 100%
|
||||
red: 0%
|
||||
green: 100%
|
||||
blue: 0%
|
||||
transition_length: 1s
|
||||
|
||||
light:
|
||||
- platform: binary
|
||||
@@ -163,3 +177,9 @@ light:
|
||||
blue: 0%
|
||||
duration: 1s
|
||||
transition_length: 500ms
|
||||
- platform: partition
|
||||
id: test_addressable_transition
|
||||
name: Addressable Transition Test
|
||||
default_transition_length: 1s
|
||||
segments:
|
||||
- single_light_id: test_rgb_light
|
||||
|
||||
@@ -15,7 +15,7 @@ lock:
|
||||
id: test_lock1
|
||||
name: Template Lock
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return LOCK_STATE_LOCKED;
|
||||
}
|
||||
return LOCK_STATE_UNLOCKED;
|
||||
|
||||
@@ -25,7 +25,7 @@ sensor:
|
||||
- platform: template
|
||||
id: template_sensor1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return 42.0;
|
||||
}
|
||||
return 0.0;
|
||||
|
||||
@@ -33,7 +33,7 @@ sensor:
|
||||
- platform: template
|
||||
id: template_sensor1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return 42.0;
|
||||
}
|
||||
return 0.0;
|
||||
@@ -46,7 +46,7 @@ text_sensor:
|
||||
- platform: template
|
||||
id: template_text_sensor1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return {"Hello World"};
|
||||
}
|
||||
return {"Goodbye (cruel) World"};
|
||||
@@ -56,7 +56,7 @@ binary_sensor:
|
||||
- platform: template
|
||||
id: template_binary_sensor1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -65,7 +65,7 @@ switch:
|
||||
- platform: template
|
||||
id: template_switch1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -79,7 +79,7 @@ cover:
|
||||
- platform: template
|
||||
id: template_cover1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return COVER_OPEN;
|
||||
}
|
||||
return COVER_CLOSED;
|
||||
@@ -88,7 +88,7 @@ lock:
|
||||
- platform: template
|
||||
id: template_lock1
|
||||
lambda: |-
|
||||
if (millis() > 10000) {
|
||||
if (esphome::millis() > 10000) {
|
||||
return LOCK_STATE_LOCKED;
|
||||
}
|
||||
return LOCK_STATE_UNLOCKED;
|
||||
|
||||
@@ -12,5 +12,8 @@ esphome:
|
||||
- logger.log: "Failed to connect to WiFi!"
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
networks:
|
||||
- ssid: MySSID
|
||||
password: password1
|
||||
- ssid: MySSID2
|
||||
password: password2
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: noise-key-test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs="
|
||||
|
||||
logger:
|
||||
@@ -49,3 +49,42 @@ async def test_noise_encryption_key_protection(
|
||||
with pytest.raises(InvalidEncryptionKeyAPIError):
|
||||
async with api_client_connected(noise_psk=wrong_key) as client:
|
||||
await client.device_info()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_noise_encryption_key_clear_protection(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that noise encryption key set in YAML cannot be changed via API."""
|
||||
# The key that's set in the YAML fixture
|
||||
noise_psk = "zX9/JHxMKwpP0jUGsF0iESCm1wRvNgR6NkKVOhn7kSs="
|
||||
|
||||
# Keep ESPHome process running throughout all tests
|
||||
async with run_compiled(yaml_config):
|
||||
# First connection - test key change attempt
|
||||
async with api_client_connected(noise_psk=noise_psk) as client:
|
||||
# Verify connection is established
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
|
||||
# Try to set a new encryption key via API
|
||||
new_key = b"" # Empty key to attempt to clear
|
||||
|
||||
# This should fail since key was set in YAML
|
||||
success = await client.noise_encryption_set_key(new_key)
|
||||
assert success is False
|
||||
|
||||
# Reconnect with the original key to verify it still works
|
||||
async with api_client_connected(noise_psk=noise_psk) as client:
|
||||
# Verify connection is still successful with original key
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "noise-key-test"
|
||||
|
||||
# Verify that connecting with a wrong key fails
|
||||
wrong_key = base64.b64encode(b"y" * 32).decode() # Different key
|
||||
with pytest.raises(InvalidEncryptionKeyAPIError):
|
||||
async with api_client_connected(noise_psk=wrong_key) as client:
|
||||
await client.device_info()
|
||||
|
||||
@@ -574,6 +574,105 @@ def test_main_filters_components_without_tests(
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
|
||||
|
||||
def test_main_detects_components_with_variant_tests(
|
||||
mock_should_run_integration_tests: Mock,
|
||||
mock_should_run_clang_tidy: Mock,
|
||||
mock_should_run_clang_format: Mock,
|
||||
mock_should_run_python_linters: Mock,
|
||||
mock_changed_files: Mock,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that components with only variant test files (test-*.yaml) are detected.
|
||||
|
||||
This test verifies the fix for components like improv_serial, ethernet, mdns,
|
||||
improv_base, and safe_mode which only have variant test files (test-*.yaml)
|
||||
instead of base test files (test.*.yaml).
|
||||
"""
|
||||
# Ensure we're not in GITHUB_ACTIONS mode for this test
|
||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||
|
||||
mock_should_run_integration_tests.return_value = False
|
||||
mock_should_run_clang_tidy.return_value = False
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = False
|
||||
|
||||
# Mock changed_files to return component files
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/components/improv_serial/improv_serial.cpp",
|
||||
"esphome/components/ethernet/ethernet.cpp",
|
||||
"esphome/components/no_tests/component.cpp",
|
||||
]
|
||||
|
||||
# Create test directory structure
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
|
||||
# improv_serial has only variant tests (like the real component)
|
||||
improv_serial_dir = tests_dir / "improv_serial"
|
||||
improv_serial_dir.mkdir(parents=True)
|
||||
(improv_serial_dir / "test-uart0.esp32-idf.yaml").write_text("test: config")
|
||||
(improv_serial_dir / "test-uart0.esp8266-ard.yaml").write_text("test: config")
|
||||
(improv_serial_dir / "test-usb_cdc.esp32-s2-idf.yaml").write_text("test: config")
|
||||
|
||||
# ethernet also has only variant tests
|
||||
ethernet_dir = tests_dir / "ethernet"
|
||||
ethernet_dir.mkdir(parents=True)
|
||||
(ethernet_dir / "test-manual_ip.esp32-idf.yaml").write_text("test: config")
|
||||
(ethernet_dir / "test-dhcp.esp32-idf.yaml").write_text("test: config")
|
||||
|
||||
# no_tests component has no test files at all
|
||||
no_tests_dir = tests_dir / "no_tests"
|
||||
no_tests_dir.mkdir(parents=True)
|
||||
|
||||
# Mock root_path to use tmp_path (need to patch both determine_jobs and helpers)
|
||||
with (
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(helpers, "root_path", str(tmp_path)),
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_changed_components",
|
||||
return_value=["improv_serial", "ethernet", "no_tests"],
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_and_test_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_components_with_dependencies",
|
||||
side_effect=lambda files, deps: (
|
||||
["improv_serial", "ethernet"]
|
||||
if not deps
|
||||
else ["improv_serial", "ethernet", "no_tests"]
|
||||
),
|
||||
),
|
||||
patch.object(determine_jobs, "changed_files", return_value=[]),
|
||||
):
|
||||
# Clear the cache since we're mocking root_path
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
determine_jobs.main()
|
||||
|
||||
# Check output
|
||||
captured = capsys.readouterr()
|
||||
output = json.loads(captured.out)
|
||||
|
||||
# changed_components should have all components
|
||||
assert set(output["changed_components"]) == {
|
||||
"improv_serial",
|
||||
"ethernet",
|
||||
"no_tests",
|
||||
}
|
||||
# changed_components_with_tests should include components with variant tests
|
||||
assert set(output["changed_components_with_tests"]) == {"improv_serial", "ethernet"}
|
||||
# component_test_count should be 2 (improv_serial and ethernet)
|
||||
assert output["component_test_count"] == 2
|
||||
# no_tests should be excluded since it has no test files
|
||||
assert "no_tests" not in output["changed_components_with_tests"]
|
||||
|
||||
|
||||
# Tests for detect_memory_impact_config function
|
||||
|
||||
|
||||
@@ -785,6 +884,51 @@ def test_detect_memory_impact_config_skips_base_bus_components(tmp_path: Path) -
|
||||
assert "i2c" not in result["components"]
|
||||
|
||||
|
||||
def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None:
|
||||
"""Test memory impact detection for components with only variant test files.
|
||||
|
||||
This verifies that memory impact analysis works correctly for components like
|
||||
improv_serial, ethernet, mdns, etc. which only have variant test files
|
||||
(test-*.yaml) instead of base test files (test.*.yaml).
|
||||
"""
|
||||
# Create test directory structure
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
|
||||
# improv_serial with only variant tests
|
||||
improv_serial_dir = tests_dir / "improv_serial"
|
||||
improv_serial_dir.mkdir(parents=True)
|
||||
(improv_serial_dir / "test-uart0.esp32-idf.yaml").write_text("test: improv")
|
||||
(improv_serial_dir / "test-uart0.esp8266-ard.yaml").write_text("test: improv")
|
||||
(improv_serial_dir / "test-usb_cdc.esp32-s2-idf.yaml").write_text("test: improv")
|
||||
|
||||
# ethernet with only variant tests
|
||||
ethernet_dir = tests_dir / "ethernet"
|
||||
ethernet_dir.mkdir(parents=True)
|
||||
(ethernet_dir / "test-manual_ip.esp32-idf.yaml").write_text("test: ethernet")
|
||||
(ethernet_dir / "test-dhcp.esp32-c3-idf.yaml").write_text("test: ethernet")
|
||||
|
||||
# Mock changed_files to return both components
|
||||
with (
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(helpers, "root_path", str(tmp_path)),
|
||||
patch.object(determine_jobs, "changed_files") as mock_changed_files,
|
||||
):
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/components/improv_serial/improv_serial.cpp",
|
||||
"esphome/components/ethernet/ethernet.cpp",
|
||||
]
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
|
||||
result = determine_jobs.detect_memory_impact_config()
|
||||
|
||||
# Should detect both components even though they only have variant tests
|
||||
assert result["should_run"] == "true"
|
||||
assert set(result["components"]) == {"improv_serial", "ethernet"}
|
||||
# Both components support esp32-idf
|
||||
assert result["platform"] == "esp32-idf"
|
||||
assert result["use_merged_config"] == "true"
|
||||
|
||||
|
||||
# Tests for clang-tidy split mode logic
|
||||
|
||||
|
||||
@@ -910,3 +1054,60 @@ def test_clang_tidy_mode_targeted_scan(
|
||||
output = json.loads(captured.out)
|
||||
|
||||
assert output["clang_tidy_mode"] == expected_mode
|
||||
|
||||
|
||||
def test_main_core_files_changed_still_detects_components(
|
||||
mock_should_run_integration_tests: Mock,
|
||||
mock_should_run_clang_tidy: Mock,
|
||||
mock_should_run_clang_format: Mock,
|
||||
mock_should_run_python_linters: Mock,
|
||||
mock_changed_files: Mock,
|
||||
mock_determine_cpp_unit_tests: Mock,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test that component changes are detected even when core files change."""
|
||||
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||
|
||||
mock_should_run_integration_tests.return_value = True
|
||||
mock_should_run_clang_tidy.return_value = True
|
||||
mock_should_run_clang_format.return_value = True
|
||||
mock_should_run_python_linters.return_value = True
|
||||
mock_determine_cpp_unit_tests.return_value = (True, [])
|
||||
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/core/helpers.h",
|
||||
"esphome/components/select/select_traits.h",
|
||||
"esphome/components/select/select_traits.cpp",
|
||||
"esphome/components/api/api.proto",
|
||||
]
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=None),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_and_test_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_components_with_dependencies",
|
||||
side_effect=lambda files, deps: (
|
||||
["select", "api"]
|
||||
if not deps
|
||||
else ["select", "api", "bluetooth_proxy", "logger"]
|
||||
),
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
captured = capsys.readouterr()
|
||||
output = json.loads(captured.out)
|
||||
|
||||
assert output["clang_tidy"] is True
|
||||
assert output["clang_tidy_mode"] == "split"
|
||||
assert "select" in output["changed_components"]
|
||||
assert "api" in output["changed_components"]
|
||||
assert len(output["changed_components"]) > 0
|
||||
|
||||
@@ -571,9 +571,11 @@ class TestEsphomeCore:
|
||||
assert target.address == "4.3.2.1"
|
||||
|
||||
def test_address__openthread(self, target):
|
||||
target.name = "test-device"
|
||||
target.config = {}
|
||||
target.config[const.CONF_OPENTHREAD] = {}
|
||||
target.config[const.CONF_OPENTHREAD] = {
|
||||
const.CONF_USE_ADDRESS: "test-device.local"
|
||||
}
|
||||
target.name = "test-device"
|
||||
|
||||
assert target.address == "test-device.local"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user