mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		| @@ -779,7 +779,7 @@ def command_update_all(args: ArgsProtocol) -> int | None: | |||||||
|         safe_print(f"{half_line}{middle_text}{half_line}") |         safe_print(f"{half_line}{middle_text}{half_line}") | ||||||
|  |  | ||||||
|     for f in files: |     for f in files: | ||||||
|         safe_print(f"Updating {color(AnsiFore.CYAN, f)}") |         safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}") | ||||||
|         safe_print("-" * twidth) |         safe_print("-" * twidth) | ||||||
|         safe_print() |         safe_print() | ||||||
|         if CORE.dashboard: |         if CORE.dashboard: | ||||||
| @@ -791,10 +791,10 @@ def command_update_all(args: ArgsProtocol) -> int | None: | |||||||
|                 "esphome", "run", f, "--no-logs", "--device", "OTA" |                 "esphome", "run", f, "--no-logs", "--device", "OTA" | ||||||
|             ) |             ) | ||||||
|         if rc == 0: |         if rc == 0: | ||||||
|             print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}") |             print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}") | ||||||
|             success[f] = True |             success[f] = True | ||||||
|         else: |         else: | ||||||
|             print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}") |             print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {str(f)}") | ||||||
|             success[f] = False |             success[f] = False | ||||||
|  |  | ||||||
|         safe_print() |         safe_print() | ||||||
| @@ -805,9 +805,9 @@ def command_update_all(args: ArgsProtocol) -> int | None: | |||||||
|     failed = 0 |     failed = 0 | ||||||
|     for f in files: |     for f in files: | ||||||
|         if success[f]: |         if success[f]: | ||||||
|             safe_print(f"  - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}") |             safe_print(f"  - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}") | ||||||
|         else: |         else: | ||||||
|             safe_print(f"  - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}") |             safe_print(f"  - {str(f)}: {color(AnsiFore.BOLD_RED, 'FAILED')}") | ||||||
|             failed += 1 |             failed += 1 | ||||||
|     return failed |     return failed | ||||||
|  |  | ||||||
|   | |||||||
| @@ -51,6 +51,7 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_OZONE, |     DEVICE_CLASS_OZONE, | ||||||
|     DEVICE_CLASS_PH, |     DEVICE_CLASS_PH, | ||||||
|     DEVICE_CLASS_PM1, |     DEVICE_CLASS_PM1, | ||||||
|  |     DEVICE_CLASS_PM4, | ||||||
|     DEVICE_CLASS_PM10, |     DEVICE_CLASS_PM10, | ||||||
|     DEVICE_CLASS_PM25, |     DEVICE_CLASS_PM25, | ||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
| @@ -116,6 +117,7 @@ DEVICE_CLASSES = [ | |||||||
|     DEVICE_CLASS_PM1, |     DEVICE_CLASS_PM1, | ||||||
|     DEVICE_CLASS_PM10, |     DEVICE_CLASS_PM10, | ||||||
|     DEVICE_CLASS_PM25, |     DEVICE_CLASS_PM25, | ||||||
|  |     DEVICE_CLASS_PM4, | ||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
|     DEVICE_CLASS_POWER_FACTOR, |     DEVICE_CLASS_POWER_FACTOR, | ||||||
|     DEVICE_CLASS_PRECIPITATION, |     DEVICE_CLASS_PRECIPITATION, | ||||||
|   | |||||||
| @@ -76,7 +76,8 @@ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len | |||||||
|     temp[raw_idx++] = data[i] >> 8; |     temp[raw_idx++] = data[i] >> 8; | ||||||
| #endif | #endif | ||||||
|     // Use MSB first since Sensirion devices use CRC-8 with MSB first |     // Use MSB first since Sensirion devices use CRC-8 with MSB first | ||||||
|     temp[raw_idx++] = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true); |     uint8_t crc = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true); | ||||||
|  |     temp[raw_idx++] = crc; | ||||||
|   } |   } | ||||||
|   this->last_error_ = this->write(temp, raw_idx); |   this->last_error_ = this->write(temp, raw_idx); | ||||||
|   return this->last_error_ == i2c::ERROR_OK; |   return this->last_error_ == i2c::ERROR_OK; | ||||||
|   | |||||||
| @@ -74,6 +74,7 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_OZONE, |     DEVICE_CLASS_OZONE, | ||||||
|     DEVICE_CLASS_PH, |     DEVICE_CLASS_PH, | ||||||
|     DEVICE_CLASS_PM1, |     DEVICE_CLASS_PM1, | ||||||
|  |     DEVICE_CLASS_PM4, | ||||||
|     DEVICE_CLASS_PM10, |     DEVICE_CLASS_PM10, | ||||||
|     DEVICE_CLASS_PM25, |     DEVICE_CLASS_PM25, | ||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
| @@ -143,6 +144,7 @@ DEVICE_CLASSES = [ | |||||||
|     DEVICE_CLASS_PM1, |     DEVICE_CLASS_PM1, | ||||||
|     DEVICE_CLASS_PM10, |     DEVICE_CLASS_PM10, | ||||||
|     DEVICE_CLASS_PM25, |     DEVICE_CLASS_PM25, | ||||||
|  |     DEVICE_CLASS_PM4, | ||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
|     DEVICE_CLASS_POWER_FACTOR, |     DEVICE_CLASS_POWER_FACTOR, | ||||||
|     DEVICE_CLASS_PRECIPITATION, |     DEVICE_CLASS_PRECIPITATION, | ||||||
|   | |||||||
| @@ -1269,6 +1269,7 @@ DEVICE_CLASS_PLUG = "plug" | |||||||
| DEVICE_CLASS_PM1 = "pm1" | DEVICE_CLASS_PM1 = "pm1" | ||||||
| DEVICE_CLASS_PM10 = "pm10" | DEVICE_CLASS_PM10 = "pm10" | ||||||
| DEVICE_CLASS_PM25 = "pm25" | DEVICE_CLASS_PM25 = "pm25" | ||||||
|  | DEVICE_CLASS_PM4 = "pm4" | ||||||
| DEVICE_CLASS_POWER = "power" | DEVICE_CLASS_POWER = "power" | ||||||
| DEVICE_CLASS_POWER_FACTOR = "power_factor" | DEVICE_CLASS_POWER_FACTOR = "power_factor" | ||||||
| DEVICE_CLASS_PRECIPITATION = "precipitation" | DEVICE_CLASS_PRECIPITATION = "precipitation" | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ from __future__ import annotations | |||||||
| from collections.abc import Generator | from collections.abc import Generator | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | import re | ||||||
| from typing import Any | from typing import Any | ||||||
| from unittest.mock import MagicMock, Mock, patch | from unittest.mock import MagicMock, Mock, patch | ||||||
|  |  | ||||||
| @@ -15,6 +16,7 @@ from esphome.__main__ import ( | |||||||
|     Purpose, |     Purpose, | ||||||
|     choose_upload_log_host, |     choose_upload_log_host, | ||||||
|     command_rename, |     command_rename, | ||||||
|  |     command_update_all, | ||||||
|     command_wizard, |     command_wizard, | ||||||
|     get_port_type, |     get_port_type, | ||||||
|     has_ip_address, |     has_ip_address, | ||||||
| @@ -55,6 +57,17 @@ from esphome.const import ( | |||||||
| from esphome.core import CORE, EsphomeError | from esphome.core import CORE, EsphomeError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def strip_ansi_codes(text: str) -> str: | ||||||
|  |     """Remove ANSI escape codes from text. | ||||||
|  |  | ||||||
|  |     This helps make test assertions cleaner by removing color codes and other | ||||||
|  |     terminal formatting that can make tests brittle. | ||||||
|  |     """ | ||||||
|  |     # Pattern to match ANSI escape sequences | ||||||
|  |     ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") | ||||||
|  |     return ansi_escape.sub("", text) | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| class MockSerialPort: | class MockSerialPort: | ||||||
|     """Mock serial port for testing. |     """Mock serial port for testing. | ||||||
| @@ -1545,3 +1558,171 @@ esp32: | |||||||
|  |  | ||||||
|     captured = capfd.readouterr() |     captured = capfd.readouterr() | ||||||
|     assert "Rename failed" in captured.out |     assert "Rename failed" in captured.out | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_command_update_all_path_string_conversion( | ||||||
|  |     tmp_path: Path, | ||||||
|  |     mock_run_external_process: Mock, | ||||||
|  |     capfd: CaptureFixture[str], | ||||||
|  | ) -> None: | ||||||
|  |     """Test that command_update_all properly converts Path objects to strings in output.""" | ||||||
|  |     yaml1 = tmp_path / "device1.yaml" | ||||||
|  |     yaml1.write_text(""" | ||||||
|  | esphome: | ||||||
|  |   name: device1 | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   board: nodemcu-32s | ||||||
|  | """) | ||||||
|  |  | ||||||
|  |     yaml2 = tmp_path / "device2.yaml" | ||||||
|  |     yaml2.write_text(""" | ||||||
|  | esphome: | ||||||
|  |   name: device2 | ||||||
|  |  | ||||||
|  | esp8266: | ||||||
|  |   board: nodemcuv2 | ||||||
|  | """) | ||||||
|  |  | ||||||
|  |     setup_core(tmp_path=tmp_path) | ||||||
|  |     mock_run_external_process.return_value = 0 | ||||||
|  |  | ||||||
|  |     assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0 | ||||||
|  |  | ||||||
|  |     captured = capfd.readouterr() | ||||||
|  |     clean_output = strip_ansi_codes(captured.out) | ||||||
|  |  | ||||||
|  |     # Check that Path objects were properly converted to strings | ||||||
|  |     # The output should contain file paths without causing TypeError | ||||||
|  |     assert "device1.yaml" in clean_output | ||||||
|  |     assert "device2.yaml" in clean_output | ||||||
|  |     assert "SUCCESS" in clean_output | ||||||
|  |     assert "SUMMARY" in clean_output | ||||||
|  |  | ||||||
|  |     # Verify run_external_process was called for each file | ||||||
|  |     assert mock_run_external_process.call_count == 2 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_command_update_all_with_failures( | ||||||
|  |     tmp_path: Path, | ||||||
|  |     mock_run_external_process: Mock, | ||||||
|  |     capfd: CaptureFixture[str], | ||||||
|  | ) -> None: | ||||||
|  |     """Test command_update_all handles mixed success/failure cases properly.""" | ||||||
|  |     yaml1 = tmp_path / "success_device.yaml" | ||||||
|  |     yaml1.write_text(""" | ||||||
|  | esphome: | ||||||
|  |   name: success_device | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   board: nodemcu-32s | ||||||
|  | """) | ||||||
|  |  | ||||||
|  |     yaml2 = tmp_path / "failed_device.yaml" | ||||||
|  |     yaml2.write_text(""" | ||||||
|  | esphome: | ||||||
|  |   name: failed_device | ||||||
|  |  | ||||||
|  | esp8266: | ||||||
|  |   board: nodemcuv2 | ||||||
|  | """) | ||||||
|  |  | ||||||
|  |     setup_core(tmp_path=tmp_path) | ||||||
|  |  | ||||||
|  |     # Mock mixed results - first succeeds, second fails | ||||||
|  |     mock_run_external_process.side_effect = [0, 1] | ||||||
|  |  | ||||||
|  |     # Should return 1 (failure) since one device failed | ||||||
|  |     assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 1 | ||||||
|  |  | ||||||
|  |     captured = capfd.readouterr() | ||||||
|  |     clean_output = strip_ansi_codes(captured.out) | ||||||
|  |  | ||||||
|  |     # Check that both success and failure are properly displayed | ||||||
|  |     assert "SUCCESS" in clean_output | ||||||
|  |     assert "ERROR" in clean_output or "FAILED" in clean_output | ||||||
|  |     assert "SUMMARY" in clean_output | ||||||
|  |  | ||||||
|  |     # Files are processed in alphabetical order, so we need to check which one succeeded/failed | ||||||
|  |     # The mock_run_external_process.side_effect = [0, 1] applies to files in alphabetical order | ||||||
|  |     # So "failed_device.yaml" gets 0 (success) and "success_device.yaml" gets 1 (failure) | ||||||
|  |     assert "failed_device.yaml: SUCCESS" in clean_output | ||||||
|  |     assert "success_device.yaml: FAILED" in clean_output | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_command_update_all_empty_directory( | ||||||
|  |     tmp_path: Path, | ||||||
|  |     mock_run_external_process: Mock, | ||||||
|  |     capfd: CaptureFixture[str], | ||||||
|  | ) -> None: | ||||||
|  |     """Test command_update_all with an empty directory (no YAML files).""" | ||||||
|  |     setup_core(tmp_path=tmp_path) | ||||||
|  |  | ||||||
|  |     assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0 | ||||||
|  |     mock_run_external_process.assert_not_called() | ||||||
|  |  | ||||||
|  |     captured = capfd.readouterr() | ||||||
|  |     clean_output = strip_ansi_codes(captured.out) | ||||||
|  |  | ||||||
|  |     assert "SUMMARY" in clean_output | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_command_update_all_single_file( | ||||||
|  |     tmp_path: Path, | ||||||
|  |     mock_run_external_process: Mock, | ||||||
|  |     capfd: CaptureFixture[str], | ||||||
|  | ) -> None: | ||||||
|  |     """Test command_update_all with a single YAML file specified.""" | ||||||
|  |     yaml_file = tmp_path / "single_device.yaml" | ||||||
|  |     yaml_file.write_text(""" | ||||||
|  | esphome: | ||||||
|  |   name: single_device | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   board: nodemcu-32s | ||||||
|  | """) | ||||||
|  |  | ||||||
|  |     setup_core(tmp_path=tmp_path) | ||||||
|  |     mock_run_external_process.return_value = 0 | ||||||
|  |  | ||||||
|  |     assert command_update_all(MockArgs(configuration=[str(yaml_file)])) == 0 | ||||||
|  |  | ||||||
|  |     captured = capfd.readouterr() | ||||||
|  |     clean_output = strip_ansi_codes(captured.out) | ||||||
|  |  | ||||||
|  |     assert "single_device.yaml" in clean_output | ||||||
|  |     assert "SUCCESS" in clean_output | ||||||
|  |     mock_run_external_process.assert_called_once() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_command_update_all_path_formatting_in_color_calls( | ||||||
|  |     tmp_path: Path, | ||||||
|  |     mock_run_external_process: Mock, | ||||||
|  |     capfd: CaptureFixture[str], | ||||||
|  | ) -> None: | ||||||
|  |     """Test that Path objects are properly converted when passed to color() function.""" | ||||||
|  |     yaml_file = tmp_path / "test-device_123.yaml" | ||||||
|  |     yaml_file.write_text(""" | ||||||
|  | esphome: | ||||||
|  |   name: test-device_123 | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   board: nodemcu-32s | ||||||
|  | """) | ||||||
|  |  | ||||||
|  |     setup_core(tmp_path=tmp_path) | ||||||
|  |     mock_run_external_process.return_value = 0 | ||||||
|  |  | ||||||
|  |     assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0 | ||||||
|  |  | ||||||
|  |     captured = capfd.readouterr() | ||||||
|  |     clean_output = strip_ansi_codes(captured.out) | ||||||
|  |  | ||||||
|  |     assert "test-device_123.yaml" in clean_output | ||||||
|  |     assert "Updating" in clean_output | ||||||
|  |     assert "SUCCESS" in clean_output | ||||||
|  |     assert "SUMMARY" in clean_output | ||||||
|  |  | ||||||
|  |     # Should not have any Python error messages | ||||||
|  |     assert "TypeError" not in clean_output | ||||||
|  |     assert "can only concatenate str" not in clean_output | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user