mirror of
https://github.com/esphome/esphome.git
synced 2025-10-31 23:21:54 +00:00
Merge remote-tracking branch 'upstream/dev' into zwave_proxy
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
# Check if destination file already exists
|
||||||
|
if os.path.exists(destination):
|
||||||
|
self.set_status(409) # Conflict status code
|
||||||
|
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_status(200)
|
||||||
self.set_header("content-type", "application/json")
|
self.set_header("content-type", "application/json")
|
||||||
self.write(json.dumps({"configuration": filename}))
|
self.write(json.dumps({"configuration": filename}))
|
||||||
self.finish()
|
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):
|
||||||
|
|||||||
@@ -189,12 +189,19 @@ 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"]
|
||||||
|
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"]
|
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 "platform" not in kwargs:
|
||||||
if board in esp8266_boards.BOARDS:
|
if board in esp8266_boards.BOARDS:
|
||||||
platform = "ESP8266"
|
platform = "ESP8266"
|
||||||
@@ -213,8 +220,14 @@ def wizard_write(path, **kwargs):
|
|||||||
return False
|
return False
|
||||||
kwargs["platform"] = platform
|
kwargs["platform"] = platform
|
||||||
hardware = kwargs["platform"]
|
hardware = kwargs["platform"]
|
||||||
|
file_text = wizard_file(**kwargs)
|
||||||
|
|
||||||
write_file(path, wizard_file(**kwargs))
|
# Check if file already exists to prevent overwriting
|
||||||
|
if os.path.exists(path) and os.path.isfile(path):
|
||||||
|
safe_print(color(AnsiFore.RED, f'The file "{path}" already exists.'))
|
||||||
|
return False
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ esphome:
|
|||||||
host:
|
host:
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
level: DEBUG
|
level: VERY_VERBOSE
|
||||||
|
|
||||||
api:
|
api:
|
||||||
actions:
|
actions:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user