1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-31 15:12:06 +00:00

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

This commit is contained in:
Keith Burzinski
2025-09-03 23:47:49 -05:00
13 changed files with 193 additions and 48 deletions

View File

@@ -1138,7 +1138,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const {
dump_field(out, "string_", this->string_); dump_field(out, "string_", this->string_);
dump_field(out, "int_", this->int_); dump_field(out, "int_", this->int_);
for (const auto it : this->bool_array) { for (const auto it : this->bool_array) {
dump_field(out, "bool_array", it, 4); dump_field(out, "bool_array", static_cast<bool>(it), 4);
} }
for (const auto &it : this->int_array) { for (const auto &it : this->int_array) {
dump_field(out, "int_array", it, 4); dump_field(out, "int_array", it, 4);

View File

@@ -80,7 +80,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(BluetoothProxy), cv.GenerateID(): cv.declare_id(BluetoothProxy),
cv.Optional(CONF_ACTIVE, default=False): cv.boolean, cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All( cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean cv.only_with_esp_idf, cv.boolean
), ),

View File

@@ -40,6 +40,7 @@ from esphome.cpp_generator import RawExpression
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
from esphome.types import ConfigType from esphome.types import ConfigType
from esphome.writer import clean_cmake_cache
from .boards import BOARDS, STANDARD_BOARDS from .boards import BOARDS, STANDARD_BOARDS
from .const import ( # noqa from .const import ( # noqa
@@ -840,6 +841,9 @@ async def to_code(config):
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
add_extra_script( add_extra_script(
"post", "post",
"post_build.py", "post_build.py",
@@ -1074,7 +1078,11 @@ def _write_idf_component_yml():
contents = yaml_util.dump({"dependencies": dependencies}) contents = yaml_util.dump({"dependencies": dependencies})
else: else:
contents = "" contents = ""
write_file_if_changed(yml_path, contents) if write_file_if_changed(yml_path, contents):
dependencies_lock = CORE.relative_build_path("dependencies.lock")
if os.path.isfile(dependencies_lock):
os.remove(dependencies_lock)
clean_cmake_cache()
# Called by writer.py # Called by writer.py

View File

@@ -47,9 +47,9 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
std::vector<uint8_t> v(len + 2); std::vector<uint8_t> v(len + 2);
v.push_back(a_register >> 8); v[0] = a_register >> 8;
v.push_back(a_register); v[1] = a_register;
v.insert(v.end(), data, data + len); std::copy(data, data + len, v.begin() + 2);
return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0);
} }

View File

@@ -18,8 +18,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str());
} }
if (!obj->traits.get_unit_of_measurement().empty()) { if (!obj->traits.get_unit_of_measurement_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement().c_str()); ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str());
} }
if (!obj->traits.get_device_class_ref().empty()) { if (!obj->traits.get_device_class_ref().empty()) {

View File

@@ -18,7 +18,7 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o
"%s Unit of Measurement: '%s'\n" "%s Unit of Measurement: '%s'\n"
"%s Accuracy Decimals: %d", "%s Accuracy Decimals: %d",
prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()), prefix, prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()), prefix,
obj->get_unit_of_measurement().c_str(), prefix, obj->get_accuracy_decimals()); obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals());
if (!obj->get_device_class_ref().empty()) { if (!obj->get_device_class_ref().empty()) {
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str());
@@ -128,7 +128,7 @@ void Sensor::internal_send_state_to_frontend(float state) {
this->set_has_state(true); this->set_has_state(true);
this->state = state; this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals()); this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
this->callback_.call(state); this->callback_.call(state);
} }

View File

@@ -51,7 +51,7 @@ from . import wpa2_eap
AUTO_LOAD = ["network"] AUTO_LOAD = ["network"]
NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2] NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
CONF_SAVE = "save" CONF_SAVE = "save"
wifi_ns = cg.esphome_ns.namespace("wifi") wifi_ns = cg.esphome_ns.namespace("wifi")
@@ -179,8 +179,8 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend(
def validate_variant(_): def validate_variant(_):
if CORE.is_esp32: if CORE.is_esp32:
variant = get_esp32_variant() variant = get_esp32_variant()
if variant in NO_WIFI_VARIANTS: if variant in NO_WIFI_VARIANTS and "esp32_hosted" not in fv.full_config.get():
raise cv.Invalid(f"{variant} does not support WiFi") raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}")
def final_validate(config): def final_validate(config):

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
import base64 import base64
import binascii
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
import datetime import datetime
import functools import functools
@@ -490,7 +491,17 @@ class WizardRequestHandler(BaseHandler):
kwargs = { kwargs = {
k: v k: v
for k, v in json.loads(self.request.body.decode()).items() for k, v in json.loads(self.request.body.decode()).items()
if k in ("name", "platform", "board", "ssid", "psk", "password") if k
in (
"type",
"name",
"platform",
"board",
"ssid",
"psk",
"password",
"file_content",
)
} }
if not kwargs["name"]: if not kwargs["name"]:
self.set_status(422) self.set_status(422)
@@ -498,19 +509,65 @@ class WizardRequestHandler(BaseHandler):
self.write(json.dumps({"error": "Name is required"})) self.write(json.dumps({"error": "Name is required"}))
return return
if "type" not in kwargs:
# Default to basic wizard type for backwards compatibility
kwargs["type"] = "basic"
kwargs["friendly_name"] = kwargs["name"] kwargs["friendly_name"] = kwargs["name"]
kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"])
if kwargs["type"] == "basic":
kwargs["ota_password"] = secrets.token_hex(16) kwargs["ota_password"] = secrets.token_hex(16)
noise_psk = secrets.token_bytes(32) noise_psk = secrets.token_bytes(32)
kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode()
elif kwargs["type"] == "upload":
try:
kwargs["file_text"] = base64.b64decode(kwargs["file_content"]).decode(
"utf-8"
)
except (binascii.Error, UnicodeDecodeError):
self.set_status(422)
self.set_header("content-type", "application/json")
self.write(
json.dumps({"error": "The uploaded file is not correctly encoded."})
)
return
elif kwargs["type"] != "empty":
self.set_status(422)
self.set_header("content-type", "application/json")
self.write(
json.dumps(
{"error": f"Invalid wizard type specified: {kwargs['type']}"}
)
)
return
filename = f"{kwargs['name']}.yaml" filename = f"{kwargs['name']}.yaml"
destination = settings.rel_path(filename) destination = settings.rel_path(filename)
wizard.wizard_write(path=destination, **kwargs)
self.set_status(200) # Check if destination file already exists
self.set_header("content-type", "application/json") if os.path.exists(destination):
self.write(json.dumps({"configuration": filename})) self.set_status(409) # Conflict status code
self.finish() self.set_header("content-type", "application/json")
self.write(
json.dumps({"error": f"Configuration file '{filename}' already exists"})
)
self.finish()
return
success = wizard.wizard_write(path=destination, **kwargs)
if success:
self.set_status(200)
self.set_header("content-type", "application/json")
self.write(json.dumps({"configuration": filename}))
self.finish()
else:
self.set_status(500)
self.set_header("content-type", "application/json")
self.write(
json.dumps(
{"error": "Failed to write configuration, see logs for details"}
)
)
self.finish()
class ImportRequestHandler(BaseHandler): class ImportRequestHandler(BaseHandler):

View File

@@ -189,32 +189,45 @@ def wizard_write(path, **kwargs):
from esphome.components.rtl87xx import boards as rtl87xx_boards from esphome.components.rtl87xx import boards as rtl87xx_boards
name = kwargs["name"] name = kwargs["name"]
board = kwargs["board"] if kwargs["type"] == "empty":
file_text = ""
# Will be updated later after editing the file
hardware = "UNKNOWN"
elif kwargs["type"] == "upload":
file_text = kwargs["file_text"]
hardware = "UNKNOWN"
else: # "basic"
board = kwargs["board"]
for key in ("ssid", "psk", "password", "ota_password"): for key in ("ssid", "psk", "password", "ota_password"):
if key in kwargs: if key in kwargs:
kwargs[key] = sanitize_double_quotes(kwargs[key]) kwargs[key] = sanitize_double_quotes(kwargs[key])
if "platform" not in kwargs:
if board in esp8266_boards.BOARDS:
platform = "ESP8266"
elif board in esp32_boards.BOARDS:
platform = "ESP32"
elif board in rp2040_boards.BOARDS:
platform = "RP2040"
elif board in bk72xx_boards.BOARDS:
platform = "BK72XX"
elif board in ln882x_boards.BOARDS:
platform = "LN882X"
elif board in rtl87xx_boards.BOARDS:
platform = "RTL87XX"
else:
safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.'))
return False
kwargs["platform"] = platform
hardware = kwargs["platform"]
file_text = wizard_file(**kwargs)
if "platform" not in kwargs: # Check if file already exists to prevent overwriting
if board in esp8266_boards.BOARDS: if os.path.exists(path) and os.path.isfile(path):
platform = "ESP8266" safe_print(color(AnsiFore.RED, f'The file "{path}" already exists.'))
elif board in esp32_boards.BOARDS: return False
platform = "ESP32"
elif board in rp2040_boards.BOARDS:
platform = "RP2040"
elif board in bk72xx_boards.BOARDS:
platform = "BK72XX"
elif board in ln882x_boards.BOARDS:
platform = "LN882X"
elif board in rtl87xx_boards.BOARDS:
platform = "RTL87XX"
else:
safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.'))
return False
kwargs["platform"] = platform
hardware = kwargs["platform"]
write_file(path, wizard_file(**kwargs)) write_file(path, file_text)
storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware) storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware)
storage_path = ext_storage_path(os.path.basename(path)) storage_path = ext_storage_path(os.path.basename(path))
storage.save(storage_path) storage.save(storage_path)

View File

@@ -310,6 +310,10 @@ def clean_build():
if os.path.isdir(piolibdeps): if os.path.isdir(piolibdeps):
_LOGGER.info("Deleting %s", piolibdeps) _LOGGER.info("Deleting %s", piolibdeps)
shutil.rmtree(piolibdeps) shutil.rmtree(piolibdeps)
dependencies_lock = CORE.relative_build_path("dependencies.lock")
if os.path.isfile(dependencies_lock):
_LOGGER.info("Deleting %s", dependencies_lock)
os.remove(dependencies_lock)
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome GITIGNORE_CONTENT = """# Gitignore settings for ESPHome

View File

@@ -1059,7 +1059,9 @@ def _generate_array_dump_content(
# Check if underlying type can use dump_field # Check if underlying type can use dump_field
if ti.can_use_dump_field(): if ti.can_use_dump_field():
# For types that have dump_field overloads, use them with extra indent # For types that have dump_field overloads, use them with extra indent
o += f' dump_field(out, "{name}", {ti.dump_field_value("it")}, 4);\n' # std::vector<bool> iterators return proxy objects, need explicit cast
value_expr = "static_cast<bool>(it)" if is_bool else ti.dump_field_value("it")
o += f' dump_field(out, "{name}", {value_expr}, 4);\n'
else: else:
# For complex types (messages, bytes), use the old pattern # For complex types (messages, bytes), use the old pattern
o += f' out.append(" {name}: ");\n' o += f' out.append(" {name}: ");\n'

View File

@@ -4,7 +4,7 @@ esphome:
host: host:
logger: logger:
level: DEBUG level: VERY_VERBOSE
api: api:
actions: actions:

View File

@@ -17,6 +17,7 @@ import esphome.wizard as wz
@pytest.fixture @pytest.fixture
def default_config(): def default_config():
return { return {
"type": "basic",
"name": "test-name", "name": "test-name",
"platform": "ESP8266", "platform": "ESP8266",
"board": "esp01_1m", "board": "esp01_1m",
@@ -125,6 +126,47 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch):
assert "esp8266:" in generated_config assert "esp8266:" in generated_config
def test_wizard_empty_config(tmp_path, monkeypatch):
"""
The wizard should be able to create an empty configuration
"""
# Given
empty_config = {
"type": "empty",
"name": "test-empty",
}
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **empty_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert generated_config == ""
def test_wizard_upload_config(tmp_path, monkeypatch):
"""
The wizard should be able to import an base64 encoded configuration
"""
# Given
empty_config = {
"type": "upload",
"name": "test-upload",
"file_text": "# imported file 📁\n\n",
}
monkeypatch.setattr(wz, "write_file", MagicMock())
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
# When
wz.wizard_write(tmp_path, **empty_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert generated_config == "# imported file 📁\n\n"
def test_wizard_write_defaults_platform_from_board_esp8266( def test_wizard_write_defaults_platform_from_board_esp8266(
default_config, tmp_path, monkeypatch default_config, tmp_path, monkeypatch
): ):
@@ -471,3 +513,22 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers):
# Then # Then
assert retval == 0 assert retval == 0
def test_wizard_write_protects_existing_config(tmpdir, default_config, monkeypatch):
"""
The wizard_write function should not overwrite existing config files and return False
"""
# Given
config_file = tmpdir.join("test.yaml")
original_content = "# Original config content\n"
config_file.write(original_content)
monkeypatch.setattr(CORE, "config_path", str(tmpdir))
# When
result = wz.wizard_write(str(config_file), **default_config)
# Then
assert result is False # Should return False when file exists
assert config_file.read() == original_content