1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-27 15:42:22 +01:00

Merge remote-tracking branch 'upstream/fix_update_all_after_path_convert' into integration

This commit is contained in:
J. Nick Koston
2025-09-21 11:06:40 -06:00
6 changed files with 193 additions and 6 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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"

View File

@@ -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