mirror of
https://github.com/esphome/esphome.git
synced 2025-09-05 04:42:21 +01:00
Merge remote-tracking branch 'upstream/dev' into ota_fixes
This commit is contained in:
@@ -90,7 +90,7 @@ def main():
|
||||
def run_command(*cmd, ignore_error: bool = False):
|
||||
print(f"$ {shlex.join(list(cmd))}")
|
||||
if not args.dry_run:
|
||||
rc = subprocess.call(list(cmd))
|
||||
rc = subprocess.call(list(cmd), close_fds=False)
|
||||
if rc != 0 and not ignore_error:
|
||||
print("Command failed")
|
||||
sys.exit(1)
|
||||
|
@@ -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)
|
||||
|
@@ -15,6 +15,7 @@ from esphome.const import (
|
||||
CONF_FRAMEWORK,
|
||||
CONF_IGNORE_EFUSE_CUSTOM_MAC,
|
||||
CONF_IGNORE_EFUSE_MAC_CRC,
|
||||
CONF_LOG_LEVEL,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM_VERSION,
|
||||
@@ -79,6 +80,15 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_RELEASE = "release"
|
||||
|
||||
LOG_LEVELS_IDF = [
|
||||
"NONE",
|
||||
"ERROR",
|
||||
"WARN",
|
||||
"INFO",
|
||||
"DEBUG",
|
||||
"VERBOSE",
|
||||
]
|
||||
|
||||
ASSERTION_LEVELS = {
|
||||
"DISABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE",
|
||||
"ENABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE",
|
||||
@@ -623,6 +633,9 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
|
||||
cv.string_strict: cv.string_strict
|
||||
},
|
||||
cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
|
||||
*LOG_LEVELS_IDF, upper=True
|
||||
),
|
||||
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
|
||||
@@ -937,6 +950,10 @@ async def to_code(config):
|
||||
),
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True
|
||||
)
|
||||
|
||||
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
|
||||
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
CONF_GROUP,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_LOG_LEVEL,
|
||||
CONF_ON_BOOT,
|
||||
CONF_ON_IDLE,
|
||||
CONF_PAGES,
|
||||
@@ -186,7 +187,7 @@ def multi_conf_validate(configs: list[dict]):
|
||||
base_config = configs[0]
|
||||
for config in configs[1:]:
|
||||
for item in (
|
||||
df.CONF_LOG_LEVEL,
|
||||
CONF_LOG_LEVEL,
|
||||
CONF_COLOR_DEPTH,
|
||||
df.CONF_BYTE_ORDER,
|
||||
df.CONF_TRANSPARENCY_KEY,
|
||||
@@ -269,11 +270,11 @@ async def to_code(configs):
|
||||
|
||||
add_define(
|
||||
"LV_LOG_LEVEL",
|
||||
f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[df.CONF_LOG_LEVEL]]}",
|
||||
f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[CONF_LOG_LEVEL]]}",
|
||||
)
|
||||
cg.add_define(
|
||||
"LVGL_LOG_LEVEL",
|
||||
cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"),
|
||||
cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"),
|
||||
)
|
||||
add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH])
|
||||
for font in helpers.lv_fonts_used:
|
||||
@@ -423,7 +424,7 @@ LVGL_SCHEMA = cv.All(
|
||||
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
|
||||
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
||||
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
||||
*df.LV_LOG_LEVELS, upper=True
|
||||
),
|
||||
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
|
||||
|
@@ -456,7 +456,6 @@ CONF_KEYPADS = "keypads"
|
||||
CONF_LAYOUT = "layout"
|
||||
CONF_LEFT_BUTTON = "left_button"
|
||||
CONF_LINE_WIDTH = "line_width"
|
||||
CONF_LOG_LEVEL = "log_level"
|
||||
CONF_LONG_PRESS_TIME = "long_press_time"
|
||||
CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time"
|
||||
CONF_LVGL_ID = "lvgl_id"
|
||||
|
@@ -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")
|
||||
|
@@ -36,7 +36,9 @@ def get_sdl_options(value):
|
||||
if value != "":
|
||||
return value
|
||||
try:
|
||||
return subprocess.check_output(["sdl2-config", "--cflags", "--libs"]).decode()
|
||||
return subprocess.check_output(
|
||||
["sdl2-config", "--cflags", "--libs"], close_fds=False
|
||||
).decode()
|
||||
except Exception as e:
|
||||
raise cv.Invalid("Unable to run sdl2-config - have you installed sdl2?") from e
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -635,15 +635,8 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
|
||||
} else if (match.method_equals("turn_on") || match.method_equals("turn_off")) {
|
||||
auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off();
|
||||
|
||||
if (request->hasParam("speed_level")) {
|
||||
auto speed_level = request->getParam("speed_level")->value();
|
||||
auto val = parse_number<int>(speed_level.c_str());
|
||||
if (!val.has_value()) {
|
||||
ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str());
|
||||
return;
|
||||
}
|
||||
call.set_speed(*val);
|
||||
}
|
||||
parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed);
|
||||
|
||||
if (request->hasParam("oscillation")) {
|
||||
auto speed = request->getParam("oscillation")->value();
|
||||
auto val = parse_on_off(speed.c_str());
|
||||
@@ -715,69 +708,26 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
request->send(200);
|
||||
} else if (match.method_equals("turn_on")) {
|
||||
auto call = obj->turn_on();
|
||||
if (request->hasParam("brightness")) {
|
||||
auto brightness = parse_number<float>(request->getParam("brightness")->value().c_str());
|
||||
if (brightness.has_value()) {
|
||||
call.set_brightness(*brightness / 255.0f);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("r")) {
|
||||
auto r = parse_number<float>(request->getParam("r")->value().c_str());
|
||||
if (r.has_value()) {
|
||||
call.set_red(*r / 255.0f);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("g")) {
|
||||
auto g = parse_number<float>(request->getParam("g")->value().c_str());
|
||||
if (g.has_value()) {
|
||||
call.set_green(*g / 255.0f);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("b")) {
|
||||
auto b = parse_number<float>(request->getParam("b")->value().c_str());
|
||||
if (b.has_value()) {
|
||||
call.set_blue(*b / 255.0f);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("white_value")) {
|
||||
auto white_value = parse_number<float>(request->getParam("white_value")->value().c_str());
|
||||
if (white_value.has_value()) {
|
||||
call.set_white(*white_value / 255.0f);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("color_temp")) {
|
||||
auto color_temp = parse_number<float>(request->getParam("color_temp")->value().c_str());
|
||||
if (color_temp.has_value()) {
|
||||
call.set_color_temperature(*color_temp);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("flash")) {
|
||||
auto flash = parse_number<uint32_t>(request->getParam("flash")->value().c_str());
|
||||
if (flash.has_value()) {
|
||||
call.set_flash_length(*flash * 1000);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("transition")) {
|
||||
auto transition = parse_number<uint32_t>(request->getParam("transition")->value().c_str());
|
||||
if (transition.has_value()) {
|
||||
call.set_transition_length(*transition * 1000);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("effect")) {
|
||||
const char *effect = request->getParam("effect")->value().c_str();
|
||||
call.set_effect(effect);
|
||||
}
|
||||
|
||||
// Parse color parameters
|
||||
parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f);
|
||||
parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f);
|
||||
parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f);
|
||||
parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f);
|
||||
parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f);
|
||||
parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature);
|
||||
|
||||
// Parse timing parameters
|
||||
parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000);
|
||||
parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000);
|
||||
|
||||
parse_string_param_(request, "effect", call, &decltype(call)::set_effect);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
} else if (match.method_equals("turn_off")) {
|
||||
auto call = obj->turn_off();
|
||||
if (request->hasParam("transition")) {
|
||||
auto transition = parse_number<uint32_t>(request->getParam("transition")->value().c_str());
|
||||
if (transition.has_value()) {
|
||||
call.set_transition_length(*transition * 1000);
|
||||
}
|
||||
}
|
||||
parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000);
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
} else {
|
||||
@@ -850,18 +800,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
|
||||
return;
|
||||
}
|
||||
|
||||
if (request->hasParam("position")) {
|
||||
auto position = parse_number<float>(request->getParam("position")->value().c_str());
|
||||
if (position.has_value()) {
|
||||
call.set_position(*position);
|
||||
}
|
||||
}
|
||||
if (request->hasParam("tilt")) {
|
||||
auto tilt = parse_number<float>(request->getParam("tilt")->value().c_str());
|
||||
if (tilt.has_value()) {
|
||||
call.set_tilt(*tilt);
|
||||
}
|
||||
}
|
||||
parse_float_param_(request, "position", call, &decltype(call)::set_position);
|
||||
parse_float_param_(request, "tilt", call, &decltype(call)::set_tilt);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -915,11 +855,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
if (request->hasParam("value")) {
|
||||
auto value = parse_number<float>(request->getParam("value")->value().c_str());
|
||||
if (value.has_value())
|
||||
call.set_value(*value);
|
||||
}
|
||||
parse_float_param_(request, "value", call, &decltype(call)::set_value);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -991,10 +927,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
|
||||
return;
|
||||
}
|
||||
|
||||
if (request->hasParam("value")) {
|
||||
std::string value = request->getParam("value")->value().c_str(); // NOLINT
|
||||
call.set_date(value);
|
||||
}
|
||||
parse_string_param_(request, "value", call, &decltype(call)::set_date);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1050,10 +983,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
|
||||
return;
|
||||
}
|
||||
|
||||
if (request->hasParam("value")) {
|
||||
std::string value = request->getParam("value")->value().c_str(); // NOLINT
|
||||
call.set_time(value);
|
||||
}
|
||||
parse_string_param_(request, "value", call, &decltype(call)::set_time);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1108,10 +1038,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
|
||||
return;
|
||||
}
|
||||
|
||||
if (request->hasParam("value")) {
|
||||
std::string value = request->getParam("value")->value().c_str(); // NOLINT
|
||||
call.set_datetime(value);
|
||||
}
|
||||
parse_string_param_(request, "value", call, &decltype(call)::set_datetime);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1162,10 +1089,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
if (request->hasParam("value")) {
|
||||
String value = request->getParam("value")->value();
|
||||
call.set_value(value.c_str()); // NOLINT
|
||||
}
|
||||
parse_string_param_(request, "value", call, &decltype(call)::set_value);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1224,11 +1148,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
if (request->hasParam("option")) {
|
||||
auto option = request->getParam("option")->value();
|
||||
call.set_option(option.c_str()); // NOLINT
|
||||
}
|
||||
parse_string_param_(request, "option", call, &decltype(call)::set_option);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1284,38 +1204,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
|
||||
auto call = obj->make_call();
|
||||
|
||||
if (request->hasParam("mode")) {
|
||||
auto mode = request->getParam("mode")->value();
|
||||
call.set_mode(mode.c_str()); // NOLINT
|
||||
}
|
||||
// Parse string mode parameters
|
||||
parse_string_param_(request, "mode", call, &decltype(call)::set_mode);
|
||||
parse_string_param_(request, "fan_mode", call, &decltype(call)::set_fan_mode);
|
||||
parse_string_param_(request, "swing_mode", call, &decltype(call)::set_swing_mode);
|
||||
|
||||
if (request->hasParam("fan_mode")) {
|
||||
auto mode = request->getParam("fan_mode")->value();
|
||||
call.set_fan_mode(mode.c_str()); // NOLINT
|
||||
}
|
||||
|
||||
if (request->hasParam("swing_mode")) {
|
||||
auto mode = request->getParam("swing_mode")->value();
|
||||
call.set_swing_mode(mode.c_str()); // NOLINT
|
||||
}
|
||||
|
||||
if (request->hasParam("target_temperature_high")) {
|
||||
auto target_temperature_high = parse_number<float>(request->getParam("target_temperature_high")->value().c_str());
|
||||
if (target_temperature_high.has_value())
|
||||
call.set_target_temperature_high(*target_temperature_high);
|
||||
}
|
||||
|
||||
if (request->hasParam("target_temperature_low")) {
|
||||
auto target_temperature_low = parse_number<float>(request->getParam("target_temperature_low")->value().c_str());
|
||||
if (target_temperature_low.has_value())
|
||||
call.set_target_temperature_low(*target_temperature_low);
|
||||
}
|
||||
|
||||
if (request->hasParam("target_temperature")) {
|
||||
auto target_temperature = parse_number<float>(request->getParam("target_temperature")->value().c_str());
|
||||
if (target_temperature.has_value())
|
||||
call.set_target_temperature(*target_temperature);
|
||||
}
|
||||
// Parse temperature parameters
|
||||
parse_float_param_(request, "target_temperature_high", call, &decltype(call)::set_target_temperature_high);
|
||||
parse_float_param_(request, "target_temperature_low", call, &decltype(call)::set_target_temperature_low);
|
||||
parse_float_param_(request, "target_temperature", call, &decltype(call)::set_target_temperature);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1506,12 +1403,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
|
||||
return;
|
||||
}
|
||||
|
||||
if (request->hasParam("position")) {
|
||||
auto position = parse_number<float>(request->getParam("position")->value().c_str());
|
||||
if (position.has_value()) {
|
||||
call.set_position(*position);
|
||||
}
|
||||
}
|
||||
parse_float_param_(request, "position", call, &decltype(call)::set_position);
|
||||
|
||||
this->defer([call]() mutable { call.perform(); });
|
||||
request->send(200);
|
||||
@@ -1559,9 +1451,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
}
|
||||
|
||||
auto call = obj->make_call();
|
||||
if (request->hasParam("code")) {
|
||||
call.set_code(request->getParam("code")->value().c_str()); // NOLINT
|
||||
}
|
||||
parse_string_param_(request, "code", call, &decltype(call)::set_code);
|
||||
|
||||
if (match.method_equals("disarm")) {
|
||||
call.disarm();
|
||||
@@ -1659,6 +1549,19 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
static const char *update_state_to_string(update::UpdateState state) {
|
||||
switch (state) {
|
||||
case update::UPDATE_STATE_NO_UPDATE:
|
||||
return "NO UPDATE";
|
||||
case update::UPDATE_STATE_AVAILABLE:
|
||||
return "UPDATE AVAILABLE";
|
||||
case update::UPDATE_STATE_INSTALLING:
|
||||
return "INSTALLING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void WebServer::on_update(update::UpdateEntity *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
@@ -1698,20 +1601,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
|
||||
return json::build_json([this, obj, start_config](JsonObject root) {
|
||||
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config);
|
||||
root["value"] = obj->update_info.latest_version;
|
||||
switch (obj->state) {
|
||||
case update::UPDATE_STATE_NO_UPDATE:
|
||||
root["state"] = "NO UPDATE";
|
||||
break;
|
||||
case update::UPDATE_STATE_AVAILABLE:
|
||||
root["state"] = "UPDATE AVAILABLE";
|
||||
break;
|
||||
case update::UPDATE_STATE_INSTALLING:
|
||||
root["state"] = "INSTALLING";
|
||||
break;
|
||||
default:
|
||||
root["state"] = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
root["state"] = update_state_to_string(obj->state);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root["current_version"] = obj->update_info.current_version;
|
||||
root["title"] = obj->update_info.title;
|
||||
|
@@ -498,6 +498,66 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
|
||||
|
||||
protected:
|
||||
void add_sorting_info_(JsonObject &root, EntityBase *entity);
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
// Helper to parse and apply a float parameter with optional scaling
|
||||
template<typename T, typename Ret>
|
||||
void parse_light_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float),
|
||||
float scale = 1.0f) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value / scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to parse and apply a uint32_t parameter with optional scaling
|
||||
template<typename T, typename Ret>
|
||||
void parse_light_param_uint_(AsyncWebServerRequest *request, const char *param_name, T &call,
|
||||
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Generic helper to parse and apply a float parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_float_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic helper to parse and apply an int parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_int_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(int)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
|
||||
if (value.has_value()) {
|
||||
(call.*setter)(*value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic helper to parse and apply a string parameter
|
||||
template<typename T, typename Ret>
|
||||
void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call,
|
||||
Ret (T::*setter)(const std::string &)) {
|
||||
if (request->hasParam(param_name)) {
|
||||
// .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);
|
||||
}
|
||||
}
|
||||
|
||||
web_server_base::WebServerBase *base_;
|
||||
#ifdef USE_ARDUINO
|
||||
DeferredUpdateEventSourceList events_;
|
||||
|
@@ -523,6 +523,7 @@ CONF_LOADED_INTEGRATIONS = "loaded_integrations"
|
||||
CONF_LOCAL = "local"
|
||||
CONF_LOCK_ACTION = "lock_action"
|
||||
CONF_LOG = "log"
|
||||
CONF_LOG_LEVEL = "log_level"
|
||||
CONF_LOG_TOPIC = "log_topic"
|
||||
CONF_LOGGER = "logger"
|
||||
CONF_LOGS = "logs"
|
||||
|
@@ -229,6 +229,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=False,
|
||||
)
|
||||
stdout_thread = threading.Thread(target=self._stdout_thread)
|
||||
stdout_thread.daemon = True
|
||||
@@ -324,14 +325,13 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
|
||||
configuration = json_message["configuration"]
|
||||
config_file = settings.rel_path(configuration)
|
||||
port = json_message["port"]
|
||||
addresses: list[str] = [port]
|
||||
addresses: list[str] = []
|
||||
if (
|
||||
port == "OTA" # pylint: disable=too-many-boolean-expressions
|
||||
and (entry := entries.get(config_file))
|
||||
and entry.loaded_integrations
|
||||
and "api" in entry.loaded_integrations
|
||||
):
|
||||
addresses = []
|
||||
# First priority: entry.address AKA use_address
|
||||
if (
|
||||
(use_address := entry.address)
|
||||
@@ -359,6 +359,13 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
|
||||
# since MQTT logging will not work otherwise
|
||||
addresses.extend(sort_ip_addresses(new_addresses))
|
||||
|
||||
if not addresses:
|
||||
# If no address was found, use the port directly
|
||||
# as otherwise they will get the chooser which
|
||||
# does not work with the dashboard as there is no
|
||||
# interactive way to get keyboard input
|
||||
addresses = [port]
|
||||
|
||||
device_args: list[str] = [
|
||||
arg for address in addresses for arg in ("--device", address)
|
||||
]
|
||||
|
@@ -17,7 +17,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def run_git_command(cmd, cwd=None) -> str:
|
||||
_LOGGER.debug("Running git command: %s", " ".join(cmd))
|
||||
try:
|
||||
ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False)
|
||||
ret = subprocess.run(
|
||||
cmd, cwd=cwd, capture_output=True, check=False, close_fds=False
|
||||
)
|
||||
except FileNotFoundError as err:
|
||||
raise cv.Invalid(
|
||||
"git is not installed but required for external_components.\n"
|
||||
|
@@ -114,7 +114,9 @@ def cpp_string_escape(string, encoding="utf-8"):
|
||||
def run_system_command(*args):
|
||||
import subprocess
|
||||
|
||||
with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
|
||||
with subprocess.Popen(
|
||||
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False
|
||||
) as p:
|
||||
stdout, stderr = p.communicate()
|
||||
rc = p.returncode
|
||||
return rc, stdout, stderr
|
||||
|
@@ -211,7 +211,7 @@ def _decode_pc(config, addr):
|
||||
return
|
||||
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
|
||||
try:
|
||||
translation = subprocess.check_output(command).decode().strip()
|
||||
translation = subprocess.check_output(command, close_fds=False).decode().strip()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
|
||||
return
|
||||
|
@@ -239,7 +239,12 @@ def run_external_process(*cmd: str, **kwargs: Any) -> int | str:
|
||||
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd, stdout=sub_stdout, stderr=sub_stderr, encoding="utf-8", check=False
|
||||
cmd,
|
||||
stdout=sub_stdout,
|
||||
stderr=sub_stderr,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
return proc.stdout if capture_stdout else proc.returncode
|
||||
except KeyboardInterrupt: # pylint: disable=try-except-raise
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -31,7 +31,11 @@ def run_format(executable, args, queue, lock, failed_files):
|
||||
invocation.append(path)
|
||||
|
||||
proc = subprocess.run(
|
||||
invocation, capture_output=True, encoding="utf-8", check=False
|
||||
invocation,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
with lock:
|
||||
|
@@ -158,7 +158,11 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files):
|
||||
invocation.extend(options)
|
||||
|
||||
proc = subprocess.run(
|
||||
invocation, capture_output=True, encoding="utf-8", check=False
|
||||
invocation,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
with lock:
|
||||
@@ -320,9 +324,11 @@ def main():
|
||||
print("Applying fixes ...")
|
||||
try:
|
||||
try:
|
||||
subprocess.call(["clang-apply-replacements-18", tmpdir])
|
||||
subprocess.call(
|
||||
["clang-apply-replacements-18", tmpdir], close_fds=False
|
||||
)
|
||||
except FileNotFoundError:
|
||||
subprocess.call(["clang-apply-replacements", tmpdir])
|
||||
subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False)
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"Error please install clang-apply-replacements-18 or clang-apply-replacements.\n",
|
||||
|
@@ -55,4 +55,6 @@ for section in config.sections():
|
||||
tools.append("-t")
|
||||
tools.append(tool)
|
||||
|
||||
subprocess.check_call(["platformio", "pkg", "install", "-g", *libs, *platforms, *tools])
|
||||
subprocess.check_call(
|
||||
["platformio", "pkg", "install", "-g", *libs, *platforms, *tools], close_fds=False
|
||||
)
|
||||
|
@@ -13,7 +13,7 @@ def find_and_activate_virtualenv():
|
||||
try:
|
||||
# Get the top-level directory of the git repository
|
||||
my_path = subprocess.check_output(
|
||||
["git", "rev-parse", "--show-toplevel"], text=True
|
||||
["git", "rev-parse", "--show-toplevel"], text=True, close_fds=False
|
||||
).strip()
|
||||
except subprocess.CalledProcessError:
|
||||
print(
|
||||
@@ -44,7 +44,7 @@ def find_and_activate_virtualenv():
|
||||
def run_command():
|
||||
# Execute the remaining arguments in the new environment
|
||||
if len(sys.argv) > 1:
|
||||
subprocess.run(sys.argv[1:], check=False)
|
||||
subprocess.run(sys.argv[1:], check=False, close_fds=False)
|
||||
else:
|
||||
print(
|
||||
"No command provided to run in the virtual environment.",
|
||||
|
@@ -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:
|
||||
|
@@ -105,6 +105,7 @@ logger:
|
||||
check=True,
|
||||
cwd=init_dir,
|
||||
env=env,
|
||||
close_fds=False,
|
||||
)
|
||||
|
||||
# Lock is held until here, ensuring cache is fully populated before any test proceeds
|
||||
@@ -245,6 +246,7 @@ async def compile_esphome(
|
||||
# Start in a new process group to isolate signal handling
|
||||
start_new_session=True,
|
||||
env=env,
|
||||
close_fds=False,
|
||||
)
|
||||
await proc.wait()
|
||||
|
||||
@@ -477,6 +479,7 @@ async def run_binary_and_wait_for_port(
|
||||
# Start in a new process group to isolate signal handling
|
||||
start_new_session=True,
|
||||
pass_fds=(device_fd,),
|
||||
close_fds=False,
|
||||
)
|
||||
|
||||
# Close the device end in the parent process
|
||||
|
Reference in New Issue
Block a user