1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-30 17:12:20 +01:00

Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2025-09-13 17:00:51 -05:00
29 changed files with 3808 additions and 114 deletions

View File

@@ -15,9 +15,11 @@ import argcomplete
from esphome import const, writer, yaml_util
import esphome.codegen as cg
from esphome.components.mqtt import CONF_DISCOVER_IP
from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import (
ALLOWED_NAME_CHARS,
CONF_API,
CONF_BAUD_RATE,
CONF_BROKER,
CONF_DEASSERT_RTS_DTR,
@@ -43,6 +45,7 @@ from esphome.const import (
SECRETS_FILES,
)
from esphome.core import CORE, EsphomeError, coroutine
from esphome.enum import StrEnum
from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import AnsiFore, color, setup_log
from esphome.types import ConfigType
@@ -106,13 +109,15 @@ def choose_prompt(options, purpose: str = None):
return options[opt - 1][1]
class Purpose(StrEnum):
UPLOADING = "uploading"
LOGGING = "logging"
def choose_upload_log_host(
default: list[str] | str | None,
check_default: str | None,
show_ota: bool,
show_mqtt: bool,
show_api: bool,
purpose: str | None = None,
purpose: Purpose,
) -> list[str]:
# Convert to list for uniform handling
defaults = [default] if isinstance(default, str) else default or []
@@ -132,13 +137,30 @@ def choose_upload_log_host(
]
resolved.append(choose_prompt(options, purpose=purpose))
elif device == "OTA":
if CORE.address and (
(show_ota and "ota" in CORE.config)
or (show_api and "api" in CORE.config)
# ensure IP adresses are used first
if is_ip_address(CORE.address) and (
(purpose == Purpose.LOGGING and has_api())
or (purpose == Purpose.UPLOADING and has_ota())
):
resolved.append(CORE.address)
elif show_mqtt and has_mqtt_logging():
resolved.append("MQTT")
if purpose == Purpose.LOGGING:
if has_api() and has_mqtt_ip_lookup():
resolved.append("MQTTIP")
if has_mqtt_logging():
resolved.append("MQTT")
if has_api() and has_non_ip_address():
resolved.append(CORE.address)
elif purpose == Purpose.UPLOADING:
if has_ota() and has_mqtt_ip_lookup():
resolved.append("MQTTIP")
if has_ota() and has_non_ip_address():
resolved.append(CORE.address)
else:
resolved.append(device)
if not resolved:
@@ -149,39 +171,111 @@ def choose_upload_log_host(
options = [
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
]
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
options.append((f"Over The Air ({CORE.address})", CORE.address))
if show_mqtt and has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if purpose == Purpose.LOGGING:
if has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if has_api():
if has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
elif purpose == Purpose.UPLOADING and has_ota():
if has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
if check_default is not None and check_default in [opt[1] for opt in options]:
return [check_default]
return [choose_prompt(options, purpose=purpose)]
def mqtt_logging_enabled(mqtt_config):
def has_mqtt_logging() -> bool:
"""Check if MQTT logging is available."""
if CONF_MQTT not in CORE.config:
return False
mqtt_config = CORE.config[CONF_MQTT]
# enabled by default
if CONF_LOG_TOPIC not in mqtt_config:
return True
log_topic = mqtt_config[CONF_LOG_TOPIC]
if log_topic is None:
return False
if CONF_TOPIC not in log_topic:
return False
return log_topic.get(CONF_LEVEL, None) != "NONE"
return log_topic[CONF_LEVEL] != "NONE"
def has_mqtt_logging() -> bool:
"""Check if MQTT logging is available."""
return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
mqtt_config
)
def has_mqtt() -> bool:
"""Check if MQTT is available."""
return CONF_MQTT in CORE.config
def has_api() -> bool:
"""Check if API is available."""
return CONF_API in CORE.config
def has_ota() -> bool:
"""Check if OTA is available."""
return CONF_OTA in CORE.config
def has_mqtt_ip_lookup() -> bool:
"""Check if MQTT is available and IP lookup is supported."""
if CONF_MQTT not in CORE.config:
return False
# Default Enabled
if CONF_DISCOVER_IP not in CORE.config[CONF_MQTT]:
return True
return CORE.config[CONF_MQTT][CONF_DISCOVER_IP]
def has_mdns() -> bool:
"""Check if MDNS is available."""
return CONF_MDNS not in CORE.config or not CORE.config[CONF_MDNS][CONF_DISABLED]
def has_non_ip_address() -> bool:
"""Check if CORE.address is set and is not an IP address."""
return CORE.address is not None and not is_ip_address(CORE.address)
def has_ip_address() -> bool:
"""Check if CORE.address is a valid IP address."""
return CORE.address is not None and is_ip_address(CORE.address)
def has_resolvable_address() -> bool:
"""Check if CORE.address is resolvable (via mDNS or is an IP address)."""
return has_mdns() or has_ip_address()
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
from esphome import mqtt
return mqtt.get_esphome_device_ip(config, username, password, client_id)
_PORT_TO_PORT_TYPE = {
"MQTT": "MQTT",
"MQTTIP": "MQTTIP",
}
def get_port_type(port: str) -> str:
if port.startswith("/") or port.startswith("COM"):
return "SERIAL"
if port == "MQTT":
return "MQTT"
return "NETWORK"
return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
def run_miniterm(config: ConfigType, port: str, args) -> int:
@@ -226,7 +320,9 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
.replace(b"\n", b"")
.decode("utf8", "backslashreplace")
)
time_str = datetime.now().time().strftime("[%H:%M:%S]")
time_ = datetime.now()
nanoseconds = time_.microsecond // 1000
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
safe_print(parser.parse_line(line, time_str))
backtrace_state = platformio_api.process_stacktrace(
@@ -437,23 +533,9 @@ def upload_program(
password = ota_conf.get(CONF_PASSWORD, "")
binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin
# Check if we should use MQTT for address resolution
# This happens when no device was specified, or the current host is "MQTT"/"OTA"
if (
CONF_MQTT in config # pylint: disable=too-many-boolean-expressions
and (not devices or host in ("MQTT", "OTA"))
and (
((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
or get_port_type(host) == "MQTT"
)
):
from esphome import mqtt
devices = [
mqtt.get_esphome_device_ip(
config, args.username, args.password, args.client_id
)
]
# MQTT address resolution
if get_port_type(host) in ("MQTT", "MQTTIP"):
devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
return espota2.run_ota(devices, remote_port, password, binary)
@@ -474,20 +556,28 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
if get_port_type(port) == "SERIAL":
check_permissions(port)
return run_miniterm(config, port, args)
if get_port_type(port) == "NETWORK" and "api" in config:
addresses_to_use = devices
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
from esphome import mqtt
mqtt_address = mqtt.get_esphome_device_ip(
port_type = get_port_type(port)
# Check if we should use API for logging
if has_api():
addresses_to_use: list[str] | None = None
if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)):
addresses_to_use = devices
elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup():
# Only use MQTT IP lookup if the first condition didn't match
# (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails)
addresses_to_use = mqtt_get_ip(
config, args.username, args.password, args.client_id
)[0]
addresses_to_use = [mqtt_address]
)
from esphome.components.api.client import run_logs
if addresses_to_use is not None:
from esphome.components.api.client import run_logs
return run_logs(config, addresses_to_use)
if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config:
return run_logs(config, addresses_to_use)
if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
from esphome import mqtt
return mqtt.show_logs(
@@ -560,10 +650,7 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
devices = choose_upload_log_host(
default=args.device,
check_default=None,
show_ota=True,
show_mqtt=False,
show_api=False,
purpose="uploading",
purpose=Purpose.UPLOADING,
)
exit_code, _ = upload_program(config, args, devices)
@@ -588,10 +675,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
devices = choose_upload_log_host(
default=args.device,
check_default=None,
show_ota=False,
show_mqtt=True,
show_api=True,
purpose="logging",
purpose=Purpose.LOGGING,
)
return show_logs(config, args, devices)
@@ -617,10 +701,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
devices = choose_upload_log_host(
default=args.device,
check_default=None,
show_ota=True,
show_mqtt=False,
show_api=True,
purpose="uploading",
purpose=Purpose.UPLOADING,
)
exit_code, successful_device = upload_program(config, args, devices)
@@ -637,10 +718,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
devices = choose_upload_log_host(
default=successful_device,
check_default=successful_device,
show_ota=False,
show_mqtt=True,
show_api=True,
purpose="logging",
purpose=Purpose.LOGGING,
)
return show_logs(config, args, devices)

View File

@@ -11,15 +11,8 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_ANALOG,
CONF_INPUT,
CONF_NUMBER,
PLATFORM_ESP8266,
PlatformFramework,
)
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
from esphome.core import CORE
CODEOWNERS = ["@esphome/core"]
@@ -273,21 +266,3 @@ def validate_adc_pin(value):
)(value)
raise NotImplementedError
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"adc_sensor_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"adc_sensor_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
}
)

View File

@@ -9,6 +9,7 @@ from esphome.components.zephyr import (
zephyr_add_prj_conf,
zephyr_add_user,
)
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_ATTENUATION,
@@ -20,6 +21,7 @@ from esphome.const import (
PLATFORM_NRF52,
STATE_CLASS_MEASUREMENT,
UNIT_VOLT,
PlatformFramework,
)
from esphome.core import CORE
@@ -174,3 +176,21 @@ async def to_code(config):
}};
"""
)
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"adc_sensor_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"adc_sensor_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
}
)

View File

@@ -62,9 +62,11 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
time_ = datetime.now()
message: bytes = msg.message
text = message.decode("utf8", "backslashreplace")
for parsed_msg in parse_log_message(
text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]"
):
nanoseconds = time_.microsecond // 1000
timestamp = (
f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
)
for parsed_msg in parse_log_message(text, timestamp):
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
stop = await async_run(cli, on_log, name=name)

View File

@@ -353,6 +353,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
# pioarduino versions that don't require a release number
# List based on https://github.com/pioarduino/esp-idf/releases
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
cv.Version(5, 5, 1),
cv.Version(5, 5, 0),
cv.Version(5, 4, 2),
cv.Version(5, 4, 1),

View File

@@ -1,7 +1,13 @@
#include "factory_reset_button.h"
#include "esphome/core/defines.h"
#ifdef USE_OPENTHREAD
#include "esphome/components/openthread/openthread.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace factory_reset {
@@ -13,9 +19,20 @@ void FactoryResetButton::press_action() {
ESP_LOGI(TAG, "Resetting");
// Let MQTT settle a bit
delay(100); // NOLINT
#ifdef USE_OPENTHREAD
openthread::global_openthread_component->on_factory_reset(FactoryResetButton::factory_reset_callback);
#else
global_preferences->reset();
App.safe_reboot();
#endif
}
#ifdef USE_OPENTHREAD
void FactoryResetButton::factory_reset_callback() {
global_preferences->reset();
App.safe_reboot();
}
#endif
} // namespace factory_reset
} // namespace esphome

View File

@@ -1,7 +1,9 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/components/button/button.h"
#include "esphome/core/component.h"
namespace esphome {
namespace factory_reset {
@@ -9,6 +11,9 @@ namespace factory_reset {
class FactoryResetButton : public button::Button, public Component {
public:
void dump_config() override;
#ifdef USE_OPENTHREAD
static void factory_reset_callback();
#endif
protected:
void press_action() override;

View File

@@ -1,7 +1,13 @@
#include "factory_reset_switch.h"
#include "esphome/core/defines.h"
#ifdef USE_OPENTHREAD
#include "esphome/components/openthread/openthread.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace factory_reset {
@@ -17,10 +23,21 @@ void FactoryResetSwitch::write_state(bool state) {
ESP_LOGI(TAG, "Resetting");
// Let MQTT settle a bit
delay(100); // NOLINT
#ifdef USE_OPENTHREAD
openthread::global_openthread_component->on_factory_reset(FactoryResetSwitch::factory_reset_callback);
#else
global_preferences->reset();
App.safe_reboot();
#endif
}
}
#ifdef USE_OPENTHREAD
void FactoryResetSwitch::factory_reset_callback() {
global_preferences->reset();
App.safe_reboot();
}
#endif
} // namespace factory_reset
} // namespace esphome

View File

@@ -1,7 +1,8 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
namespace esphome {
namespace factory_reset {
@@ -9,6 +10,9 @@ namespace factory_reset {
class FactoryResetSwitch : public switch_::Switch, public Component {
public:
void dump_config() override;
#ifdef USE_OPENTHREAD
static void factory_reset_callback();
#endif
protected:
void write_state(bool state) override;

View File

@@ -11,8 +11,6 @@
#include <openthread/instance.h>
#include <openthread/logging.h>
#include <openthread/netdata.h>
#include <openthread/srp_client.h>
#include <openthread/srp_client_buffers.h>
#include <openthread/tasklet.h>
#include <cstring>
@@ -77,8 +75,14 @@ std::optional<otIp6Address> OpenThreadComponent::get_omr_address_(InstanceLock &
return {};
}
void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrpClientService *services,
const otSrpClientService *removed_services, void *context) {
void OpenThreadComponent::defer_factory_reset_external_callback() {
ESP_LOGD(TAG, "Defer factory_reset_external_callback_");
this->defer([this]() { this->factory_reset_external_callback_(); });
}
void OpenThreadSrpComponent::srp_callback(otError err, const otSrpClientHostInfo *host_info,
const otSrpClientService *services,
const otSrpClientService *removed_services, void *context) {
if (err != 0) {
ESP_LOGW(TAG, "SRP client reported an error: %s", otThreadErrorToString(err));
for (const otSrpClientHostInfo *host = host_info; host; host = nullptr) {
@@ -90,16 +94,30 @@ void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrp
}
}
void srp_start_callback(const otSockAddr *server_socket_address, void *context) {
void OpenThreadSrpComponent::srp_start_callback(const otSockAddr *server_socket_address, void *context) {
ESP_LOGI(TAG, "SRP client has started");
}
void OpenThreadSrpComponent::srp_factory_reset_callback(otError err, const otSrpClientHostInfo *host_info,
const otSrpClientService *services,
const otSrpClientService *removed_services, void *context) {
OpenThreadComponent *obj = (OpenThreadComponent *) context;
if (err == OT_ERROR_NONE && removed_services != NULL && host_info != NULL &&
host_info->mState == OT_SRP_CLIENT_ITEM_STATE_REMOVED) {
ESP_LOGD(TAG, "Successful Removal SRP Host and Services");
} else if (err != OT_ERROR_NONE) {
// Handle other SRP client events or errors
ESP_LOGW(TAG, "SRP client event/error: %s", otThreadErrorToString(err));
}
obj->defer_factory_reset_external_callback();
}
void OpenThreadSrpComponent::setup() {
otError error;
InstanceLock lock = InstanceLock::acquire();
otInstance *instance = lock.get_instance();
otSrpClientSetCallback(instance, srp_callback, nullptr);
otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_callback, nullptr);
// set the host name
uint16_t size;
@@ -179,7 +197,8 @@ void OpenThreadSrpComponent::setup() {
ESP_LOGD(TAG, "Added service: %s", full_service.c_str());
}
otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr);
otSrpClientEnableAutoStartMode(instance, OpenThreadSrpComponent::srp_start_callback, nullptr);
ESP_LOGD(TAG, "Finished SRP setup");
}
void *OpenThreadSrpComponent::pool_alloc_(size_t size) {
@@ -217,6 +236,21 @@ bool OpenThreadComponent::teardown() {
return this->teardown_complete_;
}
void OpenThreadComponent::on_factory_reset(std::function<void()> callback) {
factory_reset_external_callback_ = callback;
ESP_LOGD(TAG, "Start Removal SRP Host and Services");
otError error;
InstanceLock lock = InstanceLock::acquire();
otInstance *instance = lock.get_instance();
otSrpClientSetCallback(instance, OpenThreadSrpComponent::srp_factory_reset_callback, this);
error = otSrpClientRemoveHostAndServices(instance, true, true);
if (error != OT_ERROR_NONE) {
ESP_LOGW(TAG, "Failed to Remove SRP Host and Services");
return;
}
ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services");
}
} // namespace openthread
} // namespace esphome

View File

@@ -6,6 +6,8 @@
#include "esphome/components/network/ip_address.h"
#include "esphome/core/component.h"
#include <openthread/srp_client.h>
#include <openthread/srp_client_buffers.h>
#include <openthread/thread.h>
#include <optional>
@@ -28,11 +30,14 @@ class OpenThreadComponent : public Component {
network::IPAddresses get_ip_addresses();
std::optional<otIp6Address> get_omr_address();
void ot_main();
void on_factory_reset(std::function<void()> callback);
void defer_factory_reset_external_callback();
protected:
std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
bool teardown_started_{false};
bool teardown_complete_{false};
std::function<void()> factory_reset_external_callback_;
};
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -43,6 +48,12 @@ class OpenThreadSrpComponent : public Component {
// This has to run after the mdns component or else no services are available to advertise
float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0; }
void setup() override;
static void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrpClientService *services,
const otSrpClientService *removed_services, void *context);
static void srp_start_callback(const otSockAddr *server_socket_address, void *context);
static void srp_factory_reset_callback(otError err, const otSrpClientHostInfo *host_info,
const otSrpClientService *services, const otSrpClientService *removed_services,
void *context);
protected:
esphome::mdns::MDNSComponent *mdns_{nullptr};

View File

@@ -270,6 +270,7 @@ void PacketTransport::add_binary_data_(uint8_t key, const char *id, bool data) {
auto len = 1 + 1 + 1 + strlen(id);
if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) {
this->flush_();
this->init_data_();
}
add(this->data_, key);
add(this->data_, (uint8_t) data);
@@ -284,6 +285,7 @@ void PacketTransport::add_data_(uint8_t key, const char *id, uint32_t data) {
auto len = 4 + 1 + 1 + strlen(id);
if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) {
this->flush_();
this->init_data_();
}
add(this->data_, key);
add(this->data_, data);

View File

@@ -114,6 +114,7 @@ CONF_AND = "and"
CONF_ANGLE = "angle"
CONF_ANY = "any"
CONF_AP = "ap"
CONF_API = "api"
CONF_APPARENT_POWER = "apparent_power"
CONF_ARDUINO_VERSION = "arduino_version"
CONF_AREA = "area"