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:
		| @@ -1138,7 +1138,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { | ||||
|   dump_field(out, "string_", this->string_); | ||||
|   dump_field(out, "int_", this->int_); | ||||
|   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) { | ||||
|     dump_field(out, "int_array", it, 4); | ||||
|   | ||||
| @@ -80,7 +80,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|         cv.Schema( | ||||
|             { | ||||
|                 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.only_with_esp_idf, cv.boolean | ||||
|                 ), | ||||
|   | ||||
| @@ -40,6 +40,7 @@ from esphome.cpp_generator import RawExpression | ||||
| import esphome.final_validate as fv | ||||
| from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | ||||
| from esphome.types import ConfigType | ||||
| from esphome.writer import clean_cmake_cache | ||||
|  | ||||
| from .boards import BOARDS, STANDARD_BOARDS | ||||
| from .const import (  # noqa | ||||
| @@ -840,6 +841,9 @@ async def to_code(config): | ||||
|     if conf[CONF_ADVANCED][CONF_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( | ||||
|         "post", | ||||
|         "post_build.py", | ||||
| @@ -1074,7 +1078,11 @@ def _write_idf_component_yml(): | ||||
|         contents = yaml_util.dump({"dependencies": dependencies}) | ||||
|     else: | ||||
|         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 | ||||
|   | ||||
| @@ -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 { | ||||
|   std::vector<uint8_t> v(len + 2); | ||||
|   v.push_back(a_register >> 8); | ||||
|   v.push_back(a_register); | ||||
|   v.insert(v.end(), data, data + len); | ||||
|   v[0] = a_register >> 8; | ||||
|   v[1] = a_register; | ||||
|   std::copy(data, data + len, v.begin() + 2); | ||||
|   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()); | ||||
|   } | ||||
|  | ||||
|   if (!obj->traits.get_unit_of_measurement().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "%s  Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement().c_str()); | ||||
|   if (!obj->traits.get_unit_of_measurement_ref().empty()) { | ||||
|     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()) { | ||||
|   | ||||
| @@ -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  Accuracy Decimals: %d", | ||||
|                 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()) { | ||||
|     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->state = 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); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -51,7 +51,7 @@ from . import wpa2_eap | ||||
|  | ||||
| AUTO_LOAD = ["network"] | ||||
|  | ||||
| NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2] | ||||
| NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] | ||||
| CONF_SAVE = "save" | ||||
|  | ||||
| wifi_ns = cg.esphome_ns.namespace("wifi") | ||||
| @@ -179,8 +179,8 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( | ||||
| def validate_variant(_): | ||||
|     if CORE.is_esp32: | ||||
|         variant = get_esp32_variant() | ||||
|         if variant in NO_WIFI_VARIANTS: | ||||
|             raise cv.Invalid(f"{variant} does not support WiFi") | ||||
|         if variant in NO_WIFI_VARIANTS and "esp32_hosted" not in fv.full_config.get(): | ||||
|             raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") | ||||
|  | ||||
|  | ||||
| def final_validate(config): | ||||
|   | ||||
| @@ -2,6 +2,7 @@ from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| import base64 | ||||
| import binascii | ||||
| from collections.abc import Callable, Iterable | ||||
| import datetime | ||||
| import functools | ||||
| @@ -490,7 +491,17 @@ class WizardRequestHandler(BaseHandler): | ||||
|         kwargs = { | ||||
|             k: v | ||||
|             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"]: | ||||
|             self.set_status(422) | ||||
| @@ -498,19 +509,65 @@ class WizardRequestHandler(BaseHandler): | ||||
|             self.write(json.dumps({"error": "Name is required"})) | ||||
|             return | ||||
|  | ||||
|         if "type" not in kwargs: | ||||
|             # Default to basic wizard type for backwards compatibility | ||||
|             kwargs["type"] = "basic" | ||||
|  | ||||
|         kwargs["friendly_name"] = kwargs["name"] | ||||
|         kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) | ||||
|  | ||||
|         kwargs["ota_password"] = secrets.token_hex(16) | ||||
|         noise_psk = secrets.token_bytes(32) | ||||
|         kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() | ||||
|         if kwargs["type"] == "basic": | ||||
|             kwargs["ota_password"] = secrets.token_hex(16) | ||||
|             noise_psk = secrets.token_bytes(32) | ||||
|             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" | ||||
|         destination = settings.rel_path(filename) | ||||
|         wizard.wizard_write(path=destination, **kwargs) | ||||
|         self.set_status(200) | ||||
|         self.set_header("content-type", "application/json") | ||||
|         self.write(json.dumps({"configuration": filename})) | ||||
|         self.finish() | ||||
|  | ||||
|         # 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_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): | ||||
|   | ||||
| @@ -189,32 +189,45 @@ def wizard_write(path, **kwargs): | ||||
|     from esphome.components.rtl87xx import boards as rtl87xx_boards | ||||
|  | ||||
|     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"): | ||||
|         if key in kwargs: | ||||
|             kwargs[key] = sanitize_double_quotes(kwargs[key]) | ||||
|         for key in ("ssid", "psk", "password", "ota_password"): | ||||
|             if key in kwargs: | ||||
|                 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: | ||||
|         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"] | ||||
|     # 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, wizard_file(**kwargs)) | ||||
|     write_file(path, file_text) | ||||
|     storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware) | ||||
|     storage_path = ext_storage_path(os.path.basename(path)) | ||||
|     storage.save(storage_path) | ||||
|   | ||||
| @@ -310,6 +310,10 @@ def clean_build(): | ||||
|     if os.path.isdir(piolibdeps): | ||||
|         _LOGGER.info("Deleting %s", 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 | ||||
|   | ||||
| @@ -1059,7 +1059,9 @@ def _generate_array_dump_content( | ||||
|     # Check if underlying type can use dump_field | ||||
|     if ti.can_use_dump_field(): | ||||
|         # 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: | ||||
|         # For complex types (messages, bytes), use the old pattern | ||||
|         o += f'  out.append("  {name}: ");\n' | ||||
|   | ||||
| @@ -4,7 +4,7 @@ esphome: | ||||
| host: | ||||
|  | ||||
| logger: | ||||
|   level: DEBUG | ||||
|   level: VERY_VERBOSE | ||||
|  | ||||
| api: | ||||
|   actions: | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import esphome.wizard as wz | ||||
| @pytest.fixture | ||||
| def default_config(): | ||||
|     return { | ||||
|         "type": "basic", | ||||
|         "name": "test-name", | ||||
|         "platform": "ESP8266", | ||||
|         "board": "esp01_1m", | ||||
| @@ -125,6 +126,47 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): | ||||
|     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( | ||||
|     default_config, tmp_path, monkeypatch | ||||
| ): | ||||
| @@ -471,3 +513,22 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): | ||||
|  | ||||
|     # Then | ||||
|     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