mirror of
https://github.com/esphome/esphome.git
synced 2025-09-04 20:32:21 +01:00
Merge branch 'integration' into memory_api
This commit is contained in:
@@ -36,6 +36,7 @@ from esphome.const import (
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
@@ -51,6 +52,20 @@ CONF_POWER_GAIN = "power_gain"
|
||||
|
||||
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(
|
||||
{
|
||||
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.GenerateID(): cv.declare_id(ADE7880),
|
||||
@@ -167,7 +239,7 @@ CONFIG_SCHEMA = (
|
||||
}
|
||||
)
|
||||
.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):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
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,
|
||||
]:
|
||||
for sensor_type in POWER_SENSOR_TYPES:
|
||||
if conf := config.get(sensor_type):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(var, f"set_{sensor_type}")(sens))
|
||||
@@ -216,44 +280,6 @@ async def power_channel(config):
|
||||
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):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
@@ -101,6 +101,38 @@ void ESP32BLETracker::loop() {
|
||||
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_();
|
||||
if (counts != this->client_state_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_));
|
||||
return;
|
||||
}
|
||||
this->cancel_timeout("scan");
|
||||
// Reset timeout state machine when stopping scan
|
||||
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
|
||||
this->set_scanner_state_(ScannerState::STOPPING);
|
||||
esp_err_t err = esp_ble_gap_stop_scanning();
|
||||
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_window = this->scan_window_;
|
||||
|
||||
// Start timeout before scan is started. Otherwise scan never starts if any error.
|
||||
this->set_timeout("scan", this->scan_duration_ * 2000, []() {
|
||||
ESP_LOGE(TAG, "Scan never terminated, rebooting to restore stack (IDF)");
|
||||
App.reboot();
|
||||
});
|
||||
// Start timeout monitoring in loop() instead of using scheduler
|
||||
// This prevents false reboots when the loop is blocked
|
||||
this->scan_start_time_ = App.get_loop_component_start_time();
|
||||
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
|
||||
|
||||
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
|
||||
if (err != ESP_OK) {
|
||||
@@ -752,7 +784,8 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
this->already_discovered_.clear();
|
||||
#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_)
|
||||
listener->on_scan_end();
|
||||
|
@@ -367,6 +367,14 @@ class ESP32BLETracker : public Component,
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
bool coex_prefer_ble_{false};
|
||||
#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
|
||||
|
@@ -20,6 +20,8 @@ namespace esphome {
|
||||
|
||||
static const char *const TAG = "esphome.ota";
|
||||
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() {
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
@@ -83,9 +85,10 @@ void ESPHomeOTAComponent::dump_config() {
|
||||
}
|
||||
|
||||
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
|
||||
// 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()) {
|
||||
this->handle_handshake_();
|
||||
}
|
||||
@@ -134,25 +137,31 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
// Try to read first byte of magic bytes
|
||||
uint8_t first_byte;
|
||||
ssize_t read = this->client_->read(&first_byte, 1);
|
||||
if (read == 1) {
|
||||
// Got the first byte, check if it's the magic byte
|
||||
if (first_byte != 0x6C) {
|
||||
ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte);
|
||||
this->cleanup_connection_();
|
||||
return;
|
||||
}
|
||||
// First byte is valid, continue with data handling
|
||||
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_();
|
||||
|
||||
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) {
|
||||
ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte);
|
||||
this->cleanup_connection_();
|
||||
return;
|
||||
}
|
||||
|
||||
// First byte is valid, continue with data handling
|
||||
this->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_)
|
||||
if (!this->readall_(buf, 4)) {
|
||||
ESP_LOGW(TAG, "Read magic bytes failed");
|
||||
this->log_read_error_("magic bytes");
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
// Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45
|
||||
@@ -192,7 +201,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
|
||||
// Read features - 1 byte
|
||||
if (!this->readall_(buf, 1)) {
|
||||
ESP_LOGW(TAG, "Read features failed");
|
||||
this->log_read_error_("features");
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
ota_features = buf[0]; // NOLINT
|
||||
@@ -271,7 +280,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
|
||||
// Read size, 4 bytes MSB first
|
||||
if (!this->readall_(buf, 4)) {
|
||||
ESP_LOGW(TAG, "Read size failed");
|
||||
this->log_read_error_("size");
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
ota_size = 0;
|
||||
@@ -303,7 +312,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
|
||||
// Read binary MD5, 32 bytes
|
||||
if (!this->readall_(buf, 32)) {
|
||||
ESP_LOGW(TAG, "Read MD5 checksum failed");
|
||||
this->log_read_error_("MD5 checksum");
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
sbuf[32] = '\0';
|
||||
@@ -378,7 +387,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
|
||||
// Read ACK
|
||||
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
|
||||
}
|
||||
|
||||
@@ -407,12 +416,12 @@ error:
|
||||
#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 at = 0;
|
||||
while (len - at > 0) {
|
||||
uint32_t now = millis();
|
||||
if (now - start > timeout) {
|
||||
if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
|
||||
ESP_LOGW(TAG, "Timeout reading %d bytes", len);
|
||||
return false;
|
||||
}
|
||||
@@ -438,12 +447,12 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len, uint32_t timeout) {
|
||||
|
||||
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 at = 0;
|
||||
while (len - at > 0) {
|
||||
uint32_t now = millis();
|
||||
if (now - start > timeout) {
|
||||
if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
|
||||
ESP_LOGW(TAG, "Timeout writing %d bytes", len);
|
||||
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_read_error_(const char *what) { ESP_LOGW(TAG, "Read %s failed", what); }
|
||||
|
||||
void ESPHomeOTAComponent::log_start_(const char *phase) {
|
||||
ESP_LOGD(TAG, "Starting %s from %s", phase, this->client_->getpeername().c_str());
|
||||
}
|
||||
|
@@ -9,9 +9,6 @@
|
||||
|
||||
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.
|
||||
class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||
public:
|
||||
@@ -32,9 +29,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||
protected:
|
||||
void handle_handshake_();
|
||||
void handle_data_();
|
||||
bool readall_(uint8_t *buf, size_t len, uint32_t timeout = OTA_SOCKET_TIMEOUT_DATA);
|
||||
bool writeall_(const 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);
|
||||
void log_socket_error_(const char *msg);
|
||||
void log_read_error_(const char *what);
|
||||
void log_start_(const char *phase);
|
||||
void cleanup_connection_();
|
||||
|
||||
@@ -43,10 +41,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
uint16_t port_;
|
||||
uint32_t client_connect_time_{0};
|
||||
|
||||
std::unique_ptr<socket::Socket> server_;
|
||||
std::unique_ptr<socket::Socket> client_;
|
||||
uint32_t client_connect_time_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
|
@@ -287,10 +287,14 @@ def angle(value):
|
||||
:param value: The input in the range 0..360
|
||||
: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")
|
||||
|
@@ -451,7 +451,8 @@ void LvglComponent::setup() {
|
||||
if (buffer == nullptr && this->buffer_frac_ == 0) {
|
||||
frac = 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) {
|
||||
this->status_set_error("Memory allocation failure");
|
||||
|
@@ -161,7 +161,7 @@ class WidgetType:
|
||||
"""
|
||||
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
|
||||
:param parent: The parent to which it should be attached
|
||||
|
@@ -439,7 +439,7 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
|
||||
:return:
|
||||
"""
|
||||
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.get_uses())
|
||||
wid = w_cnfig[CONF_ID]
|
||||
|
@@ -20,7 +20,7 @@ from ..defines import (
|
||||
CONF_START_ANGLE,
|
||||
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 ..types import LvNumber, NumberType
|
||||
from . import Widget
|
||||
@@ -29,11 +29,11 @@ CONF_ARC = "arc"
|
||||
ARC_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_VALUE): lv_float,
|
||||
cv.Optional(CONF_MIN_VALUE, default=0): cv.int_,
|
||||
cv.Optional(CONF_MAX_VALUE, default=100): cv.int_,
|
||||
cv.Optional(CONF_START_ANGLE, default=135): angle,
|
||||
cv.Optional(CONF_END_ANGLE, default=45): angle,
|
||||
cv.Optional(CONF_ROTATION, default=0.0): angle,
|
||||
cv.Optional(CONF_MIN_VALUE, default=0): lv_int,
|
||||
cv.Optional(CONF_MAX_VALUE, default=100): lv_int,
|
||||
cv.Optional(CONF_START_ANGLE, default=135): lv_angle_degrees,
|
||||
cv.Optional(CONF_END_ANGLE, default=45): lv_angle_degrees,
|
||||
cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees,
|
||||
cv.Optional(CONF_ADJUSTABLE, default=False): bool,
|
||||
cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of,
|
||||
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):
|
||||
if CONF_MIN_VALUE in config:
|
||||
lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE])
|
||||
lv.arc_set_bg_angles(
|
||||
w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10
|
||||
)
|
||||
lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10)
|
||||
max_value = await lv_int.process(config[CONF_MAX_VALUE])
|
||||
min_value = await lv_int.process(config[CONF_MIN_VALUE])
|
||||
lv.arc_set_range(w.obj, min_value, max_value)
|
||||
start = await lv_angle_degrees.process(config[CONF_START_ANGLE])
|
||||
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_change_rate(w.obj, config[CONF_CHANGE_RATE])
|
||||
|
||||
|
@@ -4,7 +4,7 @@ from esphome.const import CONF_SIZE, CONF_TEXT
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
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 ..schemas import TEXT_SCHEMA
|
||||
from ..types import WidgetType, lv_obj_t
|
||||
@@ -16,8 +16,8 @@ CONF_LIGHT_COLOR = "light_color"
|
||||
|
||||
QRCODE_SCHEMA = TEXT_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_DARK_COLOR, default="black"): color,
|
||||
cv.Optional(CONF_LIGHT_COLOR, default="white"): color,
|
||||
cv.Optional(CONF_DARK_COLOR, default="black"): lv_color,
|
||||
cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color,
|
||||
cv.Required(CONF_SIZE): cv.int_,
|
||||
}
|
||||
)
|
||||
@@ -34,11 +34,11 @@ class QrCodeType(WidgetType):
|
||||
)
|
||||
|
||||
def get_uses(self):
|
||||
return ("canvas", "img", "label")
|
||||
return "canvas", "img", "label"
|
||||
|
||||
def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
dark_color = color_retmapper(config[CONF_DARK_COLOR])
|
||||
light_color = color_retmapper(config[CONF_LIGHT_COLOR])
|
||||
async def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
dark_color = await lv_color.process(config[CONF_DARK_COLOR])
|
||||
light_color = await lv_color.process(config[CONF_LIGHT_COLOR])
|
||||
size = config[CONF_SIZE]
|
||||
return lv_expr.call("qrcode_create", parent, size, dark_color, light_color)
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import esphome.config_validation as cv
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
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 ..types import LvType
|
||||
from . import Widget, WidgetType
|
||||
@@ -12,8 +12,8 @@ CONF_SPINNER = "spinner"
|
||||
|
||||
SPINNER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ARC_LENGTH): angle,
|
||||
cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds,
|
||||
cv.Required(CONF_ARC_LENGTH): lv_angle_degrees,
|
||||
cv.Required(CONF_SPIN_TIME): lv_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -34,9 +34,9 @@ class SpinnerType(WidgetType):
|
||||
def get_uses(self):
|
||||
return (CONF_ARC,)
|
||||
|
||||
def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
spin_time = config[CONF_SPIN_TIME].total_milliseconds
|
||||
arc_length = config[CONF_ARC_LENGTH] // 10
|
||||
async def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
spin_time = await lv_milliseconds.process(config[CONF_SPIN_TIME])
|
||||
arc_length = await lv_angle_degrees.process(config[CONF_ARC_LENGTH])
|
||||
return lv_expr.call("spinner_create", parent, spin_time, arc_length)
|
||||
|
||||
|
||||
|
@@ -87,12 +87,12 @@ class TabviewType(WidgetType):
|
||||
) as content_obj:
|
||||
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(
|
||||
"tabview_create",
|
||||
parent,
|
||||
literal(config[CONF_POSITION]),
|
||||
literal(config[CONF_SIZE]),
|
||||
await DIRECTIONS.process(config[CONF_POSITION]),
|
||||
await size.process(config[CONF_SIZE]),
|
||||
)
|
||||
|
||||
|
||||
|
@@ -225,6 +225,9 @@ async def to_code(config):
|
||||
# https://github.com/Makuna/NeoPixelBus/blob/master/library.json
|
||||
# Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions
|
||||
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")
|
||||
else:
|
||||
cg.add_library("makuna/NeoPixelBus", "2.7.3")
|
||||
|
@@ -10,6 +10,7 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INVERTED,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_STATE,
|
||||
CONF_ON_TURN_OFF,
|
||||
CONF_ON_TURN_ON,
|
||||
CONF_RESTORE_MODE,
|
||||
@@ -56,6 +57,9 @@ TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action)
|
||||
SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action)
|
||||
|
||||
SwitchCondition = switch_ns.class_("SwitchCondition", Condition)
|
||||
SwitchStateTrigger = switch_ns.class_(
|
||||
"SwitchStateTrigger", automation.Trigger.template(bool)
|
||||
)
|
||||
SwitchTurnOnTrigger = switch_ns.class_(
|
||||
"SwitchTurnOnTrigger", automation.Trigger.template()
|
||||
)
|
||||
@@ -77,6 +81,11 @@ _SWITCH_SCHEMA = (
|
||||
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
|
||||
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.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:
|
||||
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, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
@@ -64,6 +64,13 @@ template<typename... Ts> class SwitchCondition : public Condition<Ts...> {
|
||||
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<> {
|
||||
public:
|
||||
SwitchTurnOnTrigger(Switch *a_switch) {
|
||||
|
@@ -552,7 +552,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
||||
void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call,
|
||||
Ret (T::*setter)(const std::string &)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -500,7 +500,8 @@ def lint_constants_usage():
|
||||
continue
|
||||
errs.append(
|
||||
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
|
||||
|
||||
|
@@ -12,12 +12,12 @@ sensor:
|
||||
frequency: 60Hz
|
||||
phase_a:
|
||||
name: Channel A
|
||||
voltage: Channel A Voltage
|
||||
current: Channel A Current
|
||||
active_power: Channel A Active Power
|
||||
power_factor: Channel A Power Factor
|
||||
forward_active_energy: Channel A Forward Active Energy
|
||||
reverse_active_energy: Channel A Reverse Active Energy
|
||||
voltage: Voltage
|
||||
current: Current
|
||||
active_power: Active Power
|
||||
power_factor: Power Factor
|
||||
forward_active_energy: Forward Active Energy
|
||||
reverse_active_energy: Reverse Active Energy
|
||||
calibration:
|
||||
current_gain: 3116628
|
||||
voltage_gain: -757178
|
||||
@@ -25,12 +25,12 @@ sensor:
|
||||
phase_angle: 188
|
||||
phase_b:
|
||||
name: Channel B
|
||||
voltage: Channel B Voltage
|
||||
current: Channel B Current
|
||||
active_power: Channel B Active Power
|
||||
power_factor: Channel B Power Factor
|
||||
forward_active_energy: Channel B Forward Active Energy
|
||||
reverse_active_energy: Channel B Reverse Active Energy
|
||||
voltage: Voltage
|
||||
current: Current
|
||||
active_power: Active Power
|
||||
power_factor: Power Factor
|
||||
forward_active_energy: Forward Active Energy
|
||||
reverse_active_energy: Reverse Active Energy
|
||||
calibration:
|
||||
current_gain: 3133655
|
||||
voltage_gain: -755235
|
||||
@@ -38,12 +38,12 @@ sensor:
|
||||
phase_angle: 188
|
||||
phase_c:
|
||||
name: Channel C
|
||||
voltage: Channel C Voltage
|
||||
current: Channel C Current
|
||||
active_power: Channel C Active Power
|
||||
power_factor: Channel C Power Factor
|
||||
forward_active_energy: Channel C Forward Active Energy
|
||||
reverse_active_energy: Channel C Reverse Active Energy
|
||||
voltage: Voltage
|
||||
current: Current
|
||||
active_power: Active Power
|
||||
power_factor: Power Factor
|
||||
forward_active_energy: Forward Active Energy
|
||||
reverse_active_energy: Reverse Active Energy
|
||||
calibration:
|
||||
current_gain: 3111158
|
||||
voltage_gain: -743813
|
||||
@@ -51,6 +51,6 @@ sensor:
|
||||
phase_angle: 180
|
||||
neutral:
|
||||
name: Neutral
|
||||
current: Neutral Current
|
||||
current: Current
|
||||
calibration:
|
||||
current_gain: 3189
|
||||
|
@@ -723,6 +723,20 @@ lvgl:
|
||||
arc_color: 0xFFFF00
|
||||
focused:
|
||||
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:
|
||||
id: bar_id
|
||||
align: top_mid
|
||||
|
@@ -9,6 +9,18 @@ switch:
|
||||
name: "Template Switch"
|
||||
id: the_switch
|
||||
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:
|
||||
on_boot:
|
||||
|
Reference in New Issue
Block a user