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}") | ||||
|  | ||||
|     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() | ||||
|         if CORE.dashboard: | ||||
| @@ -791,10 +791,10 @@ def command_update_all(args: ArgsProtocol) -> int | None: | ||||
|                 "esphome", "run", f, "--no-logs", "--device", "OTA" | ||||
|             ) | ||||
|         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 | ||||
|         else: | ||||
|             print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}") | ||||
|             print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {str(f)}") | ||||
|             success[f] = False | ||||
|  | ||||
|         safe_print() | ||||
| @@ -805,9 +805,9 @@ def command_update_all(args: ArgsProtocol) -> int | None: | ||||
|     failed = 0 | ||||
|     for f in files: | ||||
|         if success[f]: | ||||
|             safe_print(f"  - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}") | ||||
|             safe_print(f"  - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}") | ||||
|         else: | ||||
|             safe_print(f"  - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}") | ||||
|             safe_print(f"  - {str(f)}: {color(AnsiFore.BOLD_RED, 'FAILED')}") | ||||
|             failed += 1 | ||||
|     return failed | ||||
|  | ||||
|   | ||||
| @@ -51,6 +51,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_OZONE, | ||||
|     DEVICE_CLASS_PH, | ||||
|     DEVICE_CLASS_PM1, | ||||
|     DEVICE_CLASS_PM4, | ||||
|     DEVICE_CLASS_PM10, | ||||
|     DEVICE_CLASS_PM25, | ||||
|     DEVICE_CLASS_POWER, | ||||
| @@ -116,6 +117,7 @@ DEVICE_CLASSES = [ | ||||
|     DEVICE_CLASS_PM1, | ||||
|     DEVICE_CLASS_PM10, | ||||
|     DEVICE_CLASS_PM25, | ||||
|     DEVICE_CLASS_PM4, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_POWER_FACTOR, | ||||
|     DEVICE_CLASS_PRECIPITATION, | ||||
|   | ||||
| @@ -76,7 +76,8 @@ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len | ||||
|     temp[raw_idx++] = data[i] >> 8; | ||||
| #endif | ||||
|     // 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); | ||||
|   return this->last_error_ == i2c::ERROR_OK; | ||||
|   | ||||
| @@ -74,6 +74,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_OZONE, | ||||
|     DEVICE_CLASS_PH, | ||||
|     DEVICE_CLASS_PM1, | ||||
|     DEVICE_CLASS_PM4, | ||||
|     DEVICE_CLASS_PM10, | ||||
|     DEVICE_CLASS_PM25, | ||||
|     DEVICE_CLASS_POWER, | ||||
| @@ -143,6 +144,7 @@ DEVICE_CLASSES = [ | ||||
|     DEVICE_CLASS_PM1, | ||||
|     DEVICE_CLASS_PM10, | ||||
|     DEVICE_CLASS_PM25, | ||||
|     DEVICE_CLASS_PM4, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_POWER_FACTOR, | ||||
|     DEVICE_CLASS_PRECIPITATION, | ||||
|   | ||||
| @@ -1269,6 +1269,7 @@ DEVICE_CLASS_PLUG = "plug" | ||||
| DEVICE_CLASS_PM1 = "pm1" | ||||
| DEVICE_CLASS_PM10 = "pm10" | ||||
| DEVICE_CLASS_PM25 = "pm25" | ||||
| DEVICE_CLASS_PM4 = "pm4" | ||||
| DEVICE_CLASS_POWER = "power" | ||||
| DEVICE_CLASS_POWER_FACTOR = "power_factor" | ||||
| DEVICE_CLASS_PRECIPITATION = "precipitation" | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from __future__ import annotations | ||||
| from collections.abc import Generator | ||||
| from dataclasses import dataclass | ||||
| from pathlib import Path | ||||
| import re | ||||
| from typing import Any | ||||
| from unittest.mock import MagicMock, Mock, patch | ||||
|  | ||||
| @@ -15,6 +16,7 @@ from esphome.__main__ import ( | ||||
|     Purpose, | ||||
|     choose_upload_log_host, | ||||
|     command_rename, | ||||
|     command_update_all, | ||||
|     command_wizard, | ||||
|     get_port_type, | ||||
|     has_ip_address, | ||||
| @@ -55,6 +57,17 @@ from esphome.const import ( | ||||
| 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 | ||||
| class MockSerialPort: | ||||
|     """Mock serial port for testing. | ||||
| @@ -1545,3 +1558,171 @@ esp32: | ||||
|  | ||||
|     captured = capfd.readouterr() | ||||
|     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