1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-05 04:42:21 +01:00

Merge branch 'integration' into memory_api

This commit is contained in:
J. Nick Koston
2025-08-10 18:36:04 -05:00
21 changed files with 278 additions and 144 deletions

View File

@@ -36,6 +36,7 @@ from esphome.const import (
UNIT_WATT, UNIT_WATT,
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
) )
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -51,6 +52,20 @@ CONF_POWER_GAIN = "power_gain"
CONF_NEUTRAL = "neutral" CONF_NEUTRAL = "neutral"
# Tuple of power channel phases
POWER_PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C)
# Tuple of sensor types that can be configured for power channels
POWER_SENSOR_TYPES = (
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
)
NEUTRAL_CHANNEL_SCHEMA = cv.Schema( NEUTRAL_CHANNEL_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(NeutralChannel), cv.GenerateID(): cv.declare_id(NeutralChannel),
@@ -150,7 +165,64 @@ POWER_CHANNEL_SCHEMA = cv.Schema(
} }
) )
CONFIG_SCHEMA = (
def prefix_sensor_name(
sensor_conf: ConfigType,
channel_name: str,
channel_config: ConfigType,
sensor_type: str,
) -> None:
"""Helper to prefix sensor name with channel name.
Args:
sensor_conf: The sensor configuration (dict or string)
channel_name: The channel name to prefix with
channel_config: The channel configuration to update
sensor_type: The sensor type key in the channel config
"""
if isinstance(sensor_conf, dict) and CONF_NAME in sensor_conf:
sensor_name = sensor_conf[CONF_NAME]
if sensor_name and not sensor_name.startswith(channel_name):
sensor_conf[CONF_NAME] = f"{channel_name} {sensor_name}"
elif isinstance(sensor_conf, str):
# Simple value case - convert to dict with prefixed name
channel_config[sensor_type] = {CONF_NAME: f"{channel_name} {sensor_conf}"}
def process_channel_sensors(
config: ConfigType, channel_key: str, sensor_types: tuple
) -> None:
"""Process sensors for a channel and prefix their names.
Args:
config: The main configuration
channel_key: The channel key (e.g., CONF_PHASE_A, CONF_NEUTRAL)
sensor_types: Tuple of sensor types to process for this channel
"""
if not (channel_config := config.get(channel_key)) or not (
channel_name := channel_config.get(CONF_NAME)
):
return
for sensor_type in sensor_types:
if sensor_conf := channel_config.get(sensor_type):
prefix_sensor_name(sensor_conf, channel_name, channel_config, sensor_type)
def preprocess_channels(config: ConfigType) -> ConfigType:
"""Preprocess channel configurations to add channel name prefix to sensor names."""
# Process power channels
for channel in POWER_PHASES:
process_channel_sensors(config, channel, POWER_SENSOR_TYPES)
# Process neutral channel
process_channel_sensors(config, CONF_NEUTRAL, (CONF_CURRENT,))
return config
CONFIG_SCHEMA = cv.All(
preprocess_channels,
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ADE7880), cv.GenerateID(): cv.declare_id(ADE7880),
@@ -167,7 +239,7 @@ CONFIG_SCHEMA = (
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x38)) .extend(i2c.i2c_device_schema(0x38)),
) )
@@ -188,15 +260,7 @@ async def neutral_channel(config):
async def power_channel(config): async def power_channel(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
for sensor_type in [ for sensor_type in POWER_SENSOR_TYPES:
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
]:
if conf := config.get(sensor_type): if conf := config.get(sensor_type):
sens = await sensor.new_sensor(conf) sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{sensor_type}")(sens)) cg.add(getattr(var, f"set_{sensor_type}")(sens))
@@ -216,44 +280,6 @@ async def power_channel(config):
return var return var
def final_validate(config):
for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]:
if channel := config.get(channel):
channel_name = channel.get(CONF_NAME)
for sensor_type in [
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
]:
if conf := channel.get(sensor_type):
sensor_name = conf.get(CONF_NAME)
if (
sensor_name
and channel_name
and not sensor_name.startswith(channel_name)
):
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
if channel := config.get(CONF_NEUTRAL):
channel_name = channel.get(CONF_NAME)
if conf := channel.get(CONF_CURRENT):
sensor_name = conf.get(CONF_NAME)
if (
sensor_name
and channel_name
and not sensor_name.startswith(channel_name)
):
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -101,6 +101,38 @@ void ESP32BLETracker::loop() {
this->start_scan(); this->start_scan();
} }
} }
// Check for scan timeout - moved here from scheduler to avoid false reboots
// when the loop is blocked
if (this->scanner_state_ == ScannerState::RUNNING) {
switch (this->scan_timeout_state_) {
case ScanTimeoutState::MONITORING: {
uint32_t now = App.get_loop_component_start_time();
uint32_t timeout_ms = this->scan_duration_ * 2000;
// Robust time comparison that handles rollover correctly
// This works because unsigned arithmetic wraps around predictably
if ((now - this->scan_start_time_) > timeout_ms) {
// First time we've seen the timeout exceeded - wait one more loop iteration
// This ensures all components have had a chance to process pending events
// This is because esp32_ble may not have run yet and called
// gap_scan_event_handler yet when the loop unblocks
ESP_LOGW(TAG, "Scan timeout exceeded");
this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT;
}
break;
}
case ScanTimeoutState::EXCEEDED_WAIT:
// We've waited at least one full loop iteration, and scan is still running
ESP_LOGE(TAG, "Scan never terminated, rebooting");
App.reboot();
break;
case ScanTimeoutState::INACTIVE:
// This case should be unreachable - scanner and timeout states are always synchronized
break;
}
}
ClientStateCounts counts = this->count_client_states_(); ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) { if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts; this->client_state_counts_ = counts;
@@ -164,7 +196,8 @@ void ESP32BLETracker::stop_scan_() {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
return; return;
} }
this->cancel_timeout("scan"); // Reset timeout state machine when stopping scan
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
this->set_scanner_state_(ScannerState::STOPPING); this->set_scanner_state_(ScannerState::STOPPING);
esp_err_t err = esp_ble_gap_stop_scanning(); esp_err_t err = esp_ble_gap_stop_scanning();
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -197,11 +230,10 @@ void ESP32BLETracker::start_scan_(bool first) {
this->scan_params_.scan_interval = this->scan_interval_; this->scan_params_.scan_interval = this->scan_interval_;
this->scan_params_.scan_window = this->scan_window_; this->scan_params_.scan_window = this->scan_window_;
// Start timeout before scan is started. Otherwise scan never starts if any error. // Start timeout monitoring in loop() instead of using scheduler
this->set_timeout("scan", this->scan_duration_ * 2000, []() { // This prevents false reboots when the loop is blocked
ESP_LOGE(TAG, "Scan never terminated, rebooting to restore stack (IDF)"); this->scan_start_time_ = App.get_loop_component_start_time();
App.reboot(); this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
});
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -752,7 +784,8 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear(); this->already_discovered_.clear();
#endif #endif
this->cancel_timeout("scan"); // Reset timeout state machine instead of cancelling scheduler timeout
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();

View File

@@ -367,6 +367,14 @@ class ESP32BLETracker : public Component,
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
bool coex_prefer_ble_{false}; bool coex_prefer_ble_{false};
#endif #endif
// Scan timeout state machine
enum class ScanTimeoutState : uint8_t {
INACTIVE, // No timeout monitoring
MONITORING, // Actively monitoring for timeout
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
};
uint32_t scan_start_time_{0};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
}; };
// NOLINTNEXTLINE // NOLINTNEXTLINE

View File

@@ -20,6 +20,8 @@ namespace esphome {
static const char *const TAG = "esphome.ota"; static const char *const TAG = "esphome.ota";
static constexpr u_int16_t OTA_BLOCK_SIZE = 8192; static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
void ESPHomeOTAComponent::setup() { void ESPHomeOTAComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK #ifdef USE_OTA_STATE_CALLBACK
@@ -83,9 +85,10 @@ void ESPHomeOTAComponent::dump_config() {
} }
void ESPHomeOTAComponent::loop() { void ESPHomeOTAComponent::loop() {
// Skip handle_() call if no client connected and no incoming connections // Skip handle_handshake_() call if no client connected and no incoming connections
// This optimization reduces idle loop overhead when OTA is not active // This optimization reduces idle loop overhead when OTA is not active
// Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails // Note: No need to check server_ for null as the component is marked failed in setup()
// if server_ creation fails
if (this->client_ != nullptr || this->server_->ready()) { if (this->client_ != nullptr || this->server_->ready()) {
this->handle_handshake_(); this->handle_handshake_();
} }
@@ -134,25 +137,31 @@ void ESPHomeOTAComponent::handle_handshake_() {
// Try to read first byte of magic bytes // Try to read first byte of magic bytes
uint8_t first_byte; uint8_t first_byte;
ssize_t read = this->client_->read(&first_byte, 1); ssize_t read = this->client_->read(&first_byte, 1);
if (read == 1) {
// Got the first byte, check if it's the magic byte if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
return; // No data yet, try again next loop
}
if (read <= 0) {
// Error or connection closed
if (read == -1) {
this->log_socket_error_("reading first byte");
} else {
ESP_LOGW(TAG, "Remote closed during handshake");
}
this->cleanup_connection_();
return;
}
// Got first byte, check if it's the magic byte
if (first_byte != 0x6C) { if (first_byte != 0x6C) {
ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte); ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte);
this->cleanup_connection_(); this->cleanup_connection_();
return; return;
} }
// First byte is valid, continue with data handling // First byte is valid, continue with data handling
this->handle_data_(); this->handle_data_();
} else if (read == -1) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
this->log_socket_error_("reading first byte");
this->cleanup_connection_();
}
// For EAGAIN/EWOULDBLOCK, just return and try again next loop
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed connection during handshake");
this->cleanup_connection_();
}
} }
void ESPHomeOTAComponent::handle_data_() { void ESPHomeOTAComponent::handle_data_() {
@@ -173,7 +182,7 @@ void ESPHomeOTAComponent::handle_data_() {
// Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_) // Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_)
if (!this->readall_(buf, 4)) { if (!this->readall_(buf, 4)) {
ESP_LOGW(TAG, "Read magic bytes failed"); this->log_read_error_("magic bytes");
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
// Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45 // Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45
@@ -192,7 +201,7 @@ void ESPHomeOTAComponent::handle_data_() {
// Read features - 1 byte // Read features - 1 byte
if (!this->readall_(buf, 1)) { if (!this->readall_(buf, 1)) {
ESP_LOGW(TAG, "Read features failed"); this->log_read_error_("features");
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
ota_features = buf[0]; // NOLINT ota_features = buf[0]; // NOLINT
@@ -271,7 +280,7 @@ void ESPHomeOTAComponent::handle_data_() {
// Read size, 4 bytes MSB first // Read size, 4 bytes MSB first
if (!this->readall_(buf, 4)) { if (!this->readall_(buf, 4)) {
ESP_LOGW(TAG, "Read size failed"); this->log_read_error_("size");
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
ota_size = 0; ota_size = 0;
@@ -303,7 +312,7 @@ void ESPHomeOTAComponent::handle_data_() {
// Read binary MD5, 32 bytes // Read binary MD5, 32 bytes
if (!this->readall_(buf, 32)) { if (!this->readall_(buf, 32)) {
ESP_LOGW(TAG, "Read MD5 checksum failed"); this->log_read_error_("MD5 checksum");
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
sbuf[32] = '\0'; sbuf[32] = '\0';
@@ -378,7 +387,7 @@ void ESPHomeOTAComponent::handle_data_() {
// Read ACK // Read ACK
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Read ack failed"); this->log_read_error_("ack");
// do not go to error, this is not fatal // do not go to error, this is not fatal
} }
@@ -407,12 +416,12 @@ error:
#endif #endif
} }
bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len, uint32_t timeout) { bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
uint32_t start = millis(); uint32_t start = millis();
uint32_t at = 0; uint32_t at = 0;
while (len - at > 0) { while (len - at > 0) {
uint32_t now = millis(); uint32_t now = millis();
if (now - start > timeout) { if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
ESP_LOGW(TAG, "Timeout reading %d bytes", len); ESP_LOGW(TAG, "Timeout reading %d bytes", len);
return false; return false;
} }
@@ -438,12 +447,12 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len, uint32_t timeout) {
return true; return true;
} }
bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len, uint32_t timeout) { bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
uint32_t start = millis(); uint32_t start = millis();
uint32_t at = 0; uint32_t at = 0;
while (len - at > 0) { while (len - at > 0) {
uint32_t now = millis(); uint32_t now = millis();
if (now - start > timeout) { if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
ESP_LOGW(TAG, "Timeout writing %d bytes", len); ESP_LOGW(TAG, "Timeout writing %d bytes", len);
return false; return false;
} }
@@ -472,6 +481,8 @@ void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
void ESPHomeOTAComponent::log_socket_error_(const char *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", msg, errno); } void ESPHomeOTAComponent::log_socket_error_(const char *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", msg, errno); }
void ESPHomeOTAComponent::log_read_error_(const char *what) { ESP_LOGW(TAG, "Read %s failed", what); }
void ESPHomeOTAComponent::log_start_(const char *phase) { void ESPHomeOTAComponent::log_start_(const char *phase) {
ESP_LOGD(TAG, "Starting %s from %s", phase, this->client_->getpeername().c_str()); ESP_LOGD(TAG, "Starting %s from %s", phase, this->client_->getpeername().c_str());
} }

View File

@@ -9,9 +9,6 @@
namespace esphome { namespace esphome {
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
/// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA.
class ESPHomeOTAComponent : public ota::OTAComponent { class ESPHomeOTAComponent : public ota::OTAComponent {
public: public:
@@ -32,9 +29,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
protected: protected:
void handle_handshake_(); void handle_handshake_();
void handle_data_(); void handle_data_();
bool readall_(uint8_t *buf, size_t len, uint32_t timeout = OTA_SOCKET_TIMEOUT_DATA); bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len, uint32_t timeout = OTA_SOCKET_TIMEOUT_DATA); bool writeall_(const uint8_t *buf, size_t len);
void log_socket_error_(const char *msg); void log_socket_error_(const char *msg);
void log_read_error_(const char *what);
void log_start_(const char *phase); void log_start_(const char *phase);
void cleanup_connection_(); void cleanup_connection_();
@@ -43,10 +41,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
#endif // USE_OTA_PASSWORD #endif // USE_OTA_PASSWORD
uint16_t port_; uint16_t port_;
uint32_t client_connect_time_{0};
std::unique_ptr<socket::Socket> server_; std::unique_ptr<socket::Socket> server_;
std::unique_ptr<socket::Socket> client_; std::unique_ptr<socket::Socket> client_;
uint32_t client_connect_time_{0};
}; };
} // namespace esphome } // namespace esphome

View File

@@ -287,10 +287,14 @@ def angle(value):
:param value: The input in the range 0..360 :param value: The input in the range 0..360
:return: An angle in 1/10 degree units. :return: An angle in 1/10 degree units.
""" """
return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) return cv.float_range(0.0, 360.0)(cv.angle(value))
lv_angle = LValidator(angle, uint32) # Validator for angles in LVGL expressed in 1/10 degree units.
lv_angle = LValidator(angle, uint32, retmapper=lambda x: int(x * 10))
# Validator for angles in LVGL expressed in whole degrees
lv_angle_degrees = LValidator(angle, uint32, retmapper=int)
@schema_extractor("one_of") @schema_extractor("one_of")

View File

@@ -451,7 +451,8 @@ void LvglComponent::setup() {
if (buffer == nullptr && this->buffer_frac_ == 0) { if (buffer == nullptr && this->buffer_frac_ == 0) {
frac = MIN_BUFFER_FRAC; frac = MIN_BUFFER_FRAC;
buffer_pixels /= MIN_BUFFER_FRAC; buffer_pixels /= MIN_BUFFER_FRAC;
buffer = lv_custom_mem_alloc(buf_bytes / MIN_BUFFER_FRAC); // NOLINT buf_bytes /= MIN_BUFFER_FRAC;
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
} }
if (buffer == nullptr) { if (buffer == nullptr) {
this->status_set_error("Memory allocation failure"); this->status_set_error("Memory allocation failure");

View File

@@ -161,7 +161,7 @@ class WidgetType:
""" """
return [] return []
def obj_creator(self, parent: MockObjClass, config: dict): async def obj_creator(self, parent: MockObjClass, config: dict):
""" """
Create an instance of the widget type Create an instance of the widget type
:param parent: The parent to which it should be attached :param parent: The parent to which it should be attached

View File

@@ -439,7 +439,7 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
:return: :return:
""" """
spec: WidgetType = WIDGET_TYPES[w_type] spec: WidgetType = WIDGET_TYPES[w_type]
creator = spec.obj_creator(parent, w_cnfig) creator = await spec.obj_creator(parent, w_cnfig)
add_lv_use(spec.name) add_lv_use(spec.name)
add_lv_use(*spec.get_uses()) add_lv_use(*spec.get_uses())
wid = w_cnfig[CONF_ID] wid = w_cnfig[CONF_ID]

View File

@@ -20,7 +20,7 @@ from ..defines import (
CONF_START_ANGLE, CONF_START_ANGLE,
literal, literal,
) )
from ..lv_validation import angle, get_start_value, lv_float from ..lv_validation import get_start_value, lv_angle_degrees, lv_float, lv_int
from ..lvcode import lv, lv_expr, lv_obj from ..lvcode import lv, lv_expr, lv_obj
from ..types import LvNumber, NumberType from ..types import LvNumber, NumberType
from . import Widget from . import Widget
@@ -29,11 +29,11 @@ CONF_ARC = "arc"
ARC_SCHEMA = cv.Schema( ARC_SCHEMA = cv.Schema(
{ {
cv.Optional(CONF_VALUE): lv_float, cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, cv.Optional(CONF_MIN_VALUE, default=0): lv_int,
cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, cv.Optional(CONF_MAX_VALUE, default=100): lv_int,
cv.Optional(CONF_START_ANGLE, default=135): angle, cv.Optional(CONF_START_ANGLE, default=135): lv_angle_degrees,
cv.Optional(CONF_END_ANGLE, default=45): angle, cv.Optional(CONF_END_ANGLE, default=45): lv_angle_degrees,
cv.Optional(CONF_ROTATION, default=0.0): angle, cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees,
cv.Optional(CONF_ADJUSTABLE, default=False): bool, cv.Optional(CONF_ADJUSTABLE, default=False): bool,
cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of,
cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t,
@@ -59,11 +59,14 @@ class ArcType(NumberType):
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
if CONF_MIN_VALUE in config: if CONF_MIN_VALUE in config:
lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) max_value = await lv_int.process(config[CONF_MAX_VALUE])
lv.arc_set_bg_angles( min_value = await lv_int.process(config[CONF_MIN_VALUE])
w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10 lv.arc_set_range(w.obj, min_value, max_value)
) start = await lv_angle_degrees.process(config[CONF_START_ANGLE])
lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10) end = await lv_angle_degrees.process(config[CONF_END_ANGLE])
rotation = await lv_angle_degrees.process(config[CONF_ROTATION])
lv.arc_set_bg_angles(w.obj, start, end)
lv.arc_set_rotation(w.obj, rotation)
lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) lv.arc_set_mode(w.obj, literal(config[CONF_MODE]))
lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE])

View File

@@ -4,7 +4,7 @@ from esphome.const import CONF_SIZE, CONF_TEXT
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
from ..defines import CONF_MAIN from ..defines import CONF_MAIN
from ..lv_validation import color, color_retmapper, lv_text from ..lv_validation import lv_color, lv_text
from ..lvcode import LocalVariable, lv, lv_expr from ..lvcode import LocalVariable, lv, lv_expr
from ..schemas import TEXT_SCHEMA from ..schemas import TEXT_SCHEMA
from ..types import WidgetType, lv_obj_t from ..types import WidgetType, lv_obj_t
@@ -16,8 +16,8 @@ CONF_LIGHT_COLOR = "light_color"
QRCODE_SCHEMA = TEXT_SCHEMA.extend( QRCODE_SCHEMA = TEXT_SCHEMA.extend(
{ {
cv.Optional(CONF_DARK_COLOR, default="black"): color, cv.Optional(CONF_DARK_COLOR, default="black"): lv_color,
cv.Optional(CONF_LIGHT_COLOR, default="white"): color, cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color,
cv.Required(CONF_SIZE): cv.int_, cv.Required(CONF_SIZE): cv.int_,
} }
) )
@@ -34,11 +34,11 @@ class QrCodeType(WidgetType):
) )
def get_uses(self): def get_uses(self):
return ("canvas", "img", "label") return "canvas", "img", "label"
def obj_creator(self, parent: MockObjClass, config: dict): async def obj_creator(self, parent: MockObjClass, config: dict):
dark_color = color_retmapper(config[CONF_DARK_COLOR]) dark_color = await lv_color.process(config[CONF_DARK_COLOR])
light_color = color_retmapper(config[CONF_LIGHT_COLOR]) light_color = await lv_color.process(config[CONF_LIGHT_COLOR])
size = config[CONF_SIZE] size = config[CONF_SIZE]
return lv_expr.call("qrcode_create", parent, size, dark_color, light_color) return lv_expr.call("qrcode_create", parent, size, dark_color, light_color)

View File

@@ -2,7 +2,7 @@ import esphome.config_validation as cv
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME
from ..lv_validation import angle from ..lv_validation import lv_angle_degrees, lv_milliseconds
from ..lvcode import lv_expr from ..lvcode import lv_expr
from ..types import LvType from ..types import LvType
from . import Widget, WidgetType from . import Widget, WidgetType
@@ -12,8 +12,8 @@ CONF_SPINNER = "spinner"
SPINNER_SCHEMA = cv.Schema( SPINNER_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_ARC_LENGTH): angle, cv.Required(CONF_ARC_LENGTH): lv_angle_degrees,
cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds, cv.Required(CONF_SPIN_TIME): lv_milliseconds,
} }
) )
@@ -34,9 +34,9 @@ class SpinnerType(WidgetType):
def get_uses(self): def get_uses(self):
return (CONF_ARC,) return (CONF_ARC,)
def obj_creator(self, parent: MockObjClass, config: dict): async def obj_creator(self, parent: MockObjClass, config: dict):
spin_time = config[CONF_SPIN_TIME].total_milliseconds spin_time = await lv_milliseconds.process(config[CONF_SPIN_TIME])
arc_length = config[CONF_ARC_LENGTH] // 10 arc_length = await lv_angle_degrees.process(config[CONF_ARC_LENGTH])
return lv_expr.call("spinner_create", parent, spin_time, arc_length) return lv_expr.call("spinner_create", parent, spin_time, arc_length)

View File

@@ -87,12 +87,12 @@ class TabviewType(WidgetType):
) as content_obj: ) as content_obj:
await set_obj_properties(Widget(content_obj, obj_spec), content_style) await set_obj_properties(Widget(content_obj, obj_spec), content_style)
def obj_creator(self, parent: MockObjClass, config: dict): async def obj_creator(self, parent: MockObjClass, config: dict):
return lv_expr.call( return lv_expr.call(
"tabview_create", "tabview_create",
parent, parent,
literal(config[CONF_POSITION]), await DIRECTIONS.process(config[CONF_POSITION]),
literal(config[CONF_SIZE]), await size.process(config[CONF_SIZE]),
) )

View File

@@ -225,6 +225,9 @@ async def to_code(config):
# https://github.com/Makuna/NeoPixelBus/blob/master/library.json # https://github.com/Makuna/NeoPixelBus/blob/master/library.json
# Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions # Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions
if CORE.is_esp32: if CORE.is_esp32:
# disable built in rgb support as it uses the new RMT drivers and will
# conflict with NeoPixelBus which uses the legacy drivers
cg.add_build_flag("-DESP32_ARDUINO_NO_RGB_BUILTIN")
cg.add_library("makuna/NeoPixelBus", "2.8.0") cg.add_library("makuna/NeoPixelBus", "2.8.0")
else: else:
cg.add_library("makuna/NeoPixelBus", "2.7.3") cg.add_library("makuna/NeoPixelBus", "2.7.3")

View File

@@ -10,6 +10,7 @@ from esphome.const import (
CONF_ID, CONF_ID,
CONF_INVERTED, CONF_INVERTED,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_ON_STATE,
CONF_ON_TURN_OFF, CONF_ON_TURN_OFF,
CONF_ON_TURN_ON, CONF_ON_TURN_ON,
CONF_RESTORE_MODE, CONF_RESTORE_MODE,
@@ -56,6 +57,9 @@ TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action)
SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action) SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action)
SwitchCondition = switch_ns.class_("SwitchCondition", Condition) SwitchCondition = switch_ns.class_("SwitchCondition", Condition)
SwitchStateTrigger = switch_ns.class_(
"SwitchStateTrigger", automation.Trigger.template(bool)
)
SwitchTurnOnTrigger = switch_ns.class_( SwitchTurnOnTrigger = switch_ns.class_(
"SwitchTurnOnTrigger", automation.Trigger.template() "SwitchTurnOnTrigger", automation.Trigger.template()
) )
@@ -77,6 +81,11 @@ _SWITCH_SCHEMA = (
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
RESTORE_MODES, upper=True, space="_" RESTORE_MODES, upper=True, space="_"
), ),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchStateTrigger),
}
),
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger),
@@ -140,6 +149,9 @@ async def setup_switch_core_(var, config):
if (inverted := config.get(CONF_INVERTED)) is not None: if (inverted := config.get(CONF_INVERTED)) is not None:
cg.add(var.set_inverted(inverted)) cg.add(var.set_inverted(inverted))
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
for conf in config.get(CONF_ON_TURN_ON, []): for conf in config.get(CONF_ON_TURN_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)

View File

@@ -64,6 +64,13 @@ template<typename... Ts> class SwitchCondition : public Condition<Ts...> {
bool state_; bool state_;
}; };
class SwitchStateTrigger : public Trigger<bool> {
public:
SwitchStateTrigger(Switch *a_switch) {
a_switch->add_on_state_callback([this](bool state) { this->trigger(state); });
}
};
class SwitchTurnOnTrigger : public Trigger<> { class SwitchTurnOnTrigger : public Trigger<> {
public: public:
SwitchTurnOnTrigger(Switch *a_switch) { SwitchTurnOnTrigger(Switch *a_switch) {

View File

@@ -552,7 +552,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call, void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call,
Ret (T::*setter)(const std::string &)) { Ret (T::*setter)(const std::string &)) {
if (request->hasParam(param_name)) { if (request->hasParam(param_name)) {
std::string value = request->getParam(param_name)->value(); // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr)
(call.*setter)(value); (call.*setter)(value);
} }
} }

View File

@@ -500,7 +500,8 @@ def lint_constants_usage():
continue continue
errs.append( errs.append(
f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the "
f"constant to const.py (Uses: {', '.join(uses)})" f"constant to const.py (Uses: {', '.join(uses)}) in a separate PR. "
"See https://developers.esphome.io/contributing/code/#python"
) )
return errs return errs

View File

@@ -12,12 +12,12 @@ sensor:
frequency: 60Hz frequency: 60Hz
phase_a: phase_a:
name: Channel A name: Channel A
voltage: Channel A Voltage voltage: Voltage
current: Channel A Current current: Current
active_power: Channel A Active Power active_power: Active Power
power_factor: Channel A Power Factor power_factor: Power Factor
forward_active_energy: Channel A Forward Active Energy forward_active_energy: Forward Active Energy
reverse_active_energy: Channel A Reverse Active Energy reverse_active_energy: Reverse Active Energy
calibration: calibration:
current_gain: 3116628 current_gain: 3116628
voltage_gain: -757178 voltage_gain: -757178
@@ -25,12 +25,12 @@ sensor:
phase_angle: 188 phase_angle: 188
phase_b: phase_b:
name: Channel B name: Channel B
voltage: Channel B Voltage voltage: Voltage
current: Channel B Current current: Current
active_power: Channel B Active Power active_power: Active Power
power_factor: Channel B Power Factor power_factor: Power Factor
forward_active_energy: Channel B Forward Active Energy forward_active_energy: Forward Active Energy
reverse_active_energy: Channel B Reverse Active Energy reverse_active_energy: Reverse Active Energy
calibration: calibration:
current_gain: 3133655 current_gain: 3133655
voltage_gain: -755235 voltage_gain: -755235
@@ -38,12 +38,12 @@ sensor:
phase_angle: 188 phase_angle: 188
phase_c: phase_c:
name: Channel C name: Channel C
voltage: Channel C Voltage voltage: Voltage
current: Channel C Current current: Current
active_power: Channel C Active Power active_power: Active Power
power_factor: Channel C Power Factor power_factor: Power Factor
forward_active_energy: Channel C Forward Active Energy forward_active_energy: Forward Active Energy
reverse_active_energy: Channel C Reverse Active Energy reverse_active_energy: Reverse Active Energy
calibration: calibration:
current_gain: 3111158 current_gain: 3111158
voltage_gain: -743813 voltage_gain: -743813
@@ -51,6 +51,6 @@ sensor:
phase_angle: 180 phase_angle: 180
neutral: neutral:
name: Neutral name: Neutral
current: Neutral Current current: Current
calibration: calibration:
current_gain: 3189 current_gain: 3189

View File

@@ -723,6 +723,20 @@ lvgl:
arc_color: 0xFFFF00 arc_color: 0xFFFF00
focused: focused:
arc_color: 0x808080 arc_color: 0x808080
- arc:
align: center
id: lv_arc_1
value: !lambda return 75;
min_value: !lambda return 50;
max_value: !lambda return 60;
arc_color: 0xFF0000
indicator:
arc_width: !lambda return 20;
arc_color: 0xF000FF
pressed:
arc_color: 0xFFFF00
focused:
arc_color: 0x808080
- bar: - bar:
id: bar_id id: bar_id
align: top_mid align: top_mid

View File

@@ -9,6 +9,18 @@ switch:
name: "Template Switch" name: "Template Switch"
id: the_switch id: the_switch
optimistic: true optimistic: true
on_state:
- if:
condition:
- lambda: return x;
then:
- logger.log: "Switch turned ON"
else:
- logger.log: "Switch turned OFF"
on_turn_on:
- logger.log: "Switch is now ON"
on_turn_off:
- logger.log: "Switch is now OFF"
esphome: esphome:
on_boot: on_boot: