mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'dev' into usb-uart
This commit is contained in:
		| @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from esphome.components.packages import do_packages_pass | ||||
| from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass | ||||
| from esphome.config import resolve_extend_remove | ||||
| from esphome.config_helpers import Extend, Remove | ||||
| import esphome.config_validation as cv | ||||
| @@ -94,6 +94,50 @@ def test_package_invalid_dict(basic_esphome, basic_wifi): | ||||
|         packages_pass(config) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "package", | ||||
|     [ | ||||
|         {"package1": "github://esphome/non-existant-repo/file1.yml@main"}, | ||||
|         {"package2": "github://esphome/non-existant-repo/file1.yml"}, | ||||
|         {"package3": "github://esphome/non-existant-repo/other-folder/file1.yml"}, | ||||
|         [ | ||||
|             "github://esphome/non-existant-repo/file1.yml@main", | ||||
|             "github://esphome/non-existant-repo/file1.yml", | ||||
|             "github://esphome/non-existant-repo/other-folder/file1.yml", | ||||
|         ], | ||||
|     ], | ||||
| ) | ||||
| def test_package_shorthand(package): | ||||
|     CONFIG_SCHEMA(package) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     "package", | ||||
|     [ | ||||
|         # not github | ||||
|         {"package1": "someplace://esphome/non-existant-repo/file1.yml@main"}, | ||||
|         # missing repo | ||||
|         {"package2": "github://esphome/file1.yml"}, | ||||
|         # missing file | ||||
|         {"package3": "github://esphome/non-existant-repo/@main"}, | ||||
|         {"a": "invalid string, not shorthand"}, | ||||
|         "some string", | ||||
|         3, | ||||
|         False, | ||||
|         {"a": 8}, | ||||
|         ["someplace://esphome/non-existant-repo/file1.yml@main"], | ||||
|         ["github://esphome/file1.yml"], | ||||
|         ["github://esphome/non-existant-repo/@main"], | ||||
|         ["some string"], | ||||
|         [True], | ||||
|         [3], | ||||
|     ], | ||||
| ) | ||||
| def test_package_invalid(package): | ||||
|     with pytest.raises(cv.Invalid): | ||||
|         CONFIG_SCHEMA(package) | ||||
|  | ||||
|  | ||||
| def test_package_include(basic_wifi, basic_esphome): | ||||
|     """ | ||||
|     Tests the simple case where an independent config present in a package is added to the top-level config as is. | ||||
|   | ||||
| @@ -3,3 +3,52 @@ esp32_ble_tracker: | ||||
| ble_client: | ||||
|   - mac_address: 01:02:03:04:05:06 | ||||
|     id: test_blec | ||||
|     on_connect: | ||||
|       - ble_client.ble_write: | ||||
|           id: test_blec | ||||
|           service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" | ||||
|           characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" | ||||
|           value: !lambda |- | ||||
|             return std::vector<uint8_t>{0x01, 0x02, 0x03}; | ||||
|       - ble_client.ble_write: | ||||
|           id: test_blec | ||||
|           service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" | ||||
|           characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" | ||||
|           value: [0x04, 0x05, 0x06] | ||||
|     on_passkey_request: | ||||
|       - ble_client.passkey_reply: | ||||
|           id: test_blec | ||||
|           passkey: !lambda |- | ||||
|             return 123456; | ||||
|       - ble_client.passkey_reply: | ||||
|           id: test_blec | ||||
|           passkey: 654321 | ||||
|     on_numeric_comparison_request: | ||||
|       - ble_client.numeric_comparison_reply: | ||||
|           id: test_blec | ||||
|           accept: !lambda |- | ||||
|             return true; | ||||
|       - ble_client.numeric_comparison_reply: | ||||
|           id: test_blec | ||||
|           accept: false | ||||
|  | ||||
| sensor: | ||||
|   - platform: ble_client | ||||
|     ble_client_id: test_blec | ||||
|     type: characteristic | ||||
|     id: test_sensor_lambda | ||||
|     name: "BLE Sensor with Lambda" | ||||
|     service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" | ||||
|     characteristic_uuid: "abcd1236-abcd-1234-abcd-abcd12345678" | ||||
|     lambda: |- | ||||
|       if (x.size() >= 2) { | ||||
|         return (float)(x[0] | (x[1] << 8)) / 100.0; | ||||
|       } | ||||
|       return NAN; | ||||
|   - platform: ble_client | ||||
|     ble_client_id: test_blec | ||||
|     type: characteristic | ||||
|     id: test_sensor_no_lambda | ||||
|     name: "BLE Sensor without Lambda" | ||||
|     service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" | ||||
|     characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678" | ||||
|   | ||||
| @@ -4,51 +4,6 @@ wifi: | ||||
|   ssid: MySSID | ||||
|   password: password1 | ||||
|  | ||||
| esphome: | ||||
|   on_boot: | ||||
|     then: | ||||
|       - http_request.get: | ||||
|           url: https://esphome.io | ||||
|           request_headers: | ||||
|             Content-Type: application/json | ||||
|           collect_headers: | ||||
|             - age | ||||
|           on_error: | ||||
|             logger.log: "Request failed" | ||||
|           on_response: | ||||
|             then: | ||||
|               - logger.log: | ||||
|                   format: "Response status: %d, Duration: %lu ms, age: %s" | ||||
|                   args: | ||||
|                     - response->status_code | ||||
|                     - (long) response->duration_ms | ||||
|                     - response->get_response_header("age").c_str() | ||||
|       - http_request.post: | ||||
|           url: https://esphome.io | ||||
|           request_headers: | ||||
|             Content-Type: application/json | ||||
|           json: | ||||
|             key: value | ||||
|       - http_request.send: | ||||
|           method: PUT | ||||
|           url: https://esphome.io | ||||
|           request_headers: | ||||
|             Content-Type: application/json | ||||
|           body: "Some data" | ||||
|  | ||||
| http_request: | ||||
|   useragent: esphome/tagreader | ||||
|   timeout: 10s | ||||
|   verify_ssl: ${verify_ssl} | ||||
|  | ||||
| script: | ||||
|   - id: does_not_compile | ||||
|     parameters: | ||||
|       api_url: string | ||||
|     then: | ||||
|       - http_request.get: | ||||
|           url: "http://google.com" | ||||
|  | ||||
| ota: | ||||
|   - platform: http_request | ||||
|     id: http_request_ota | ||||
|   | ||||
| @@ -31,6 +31,20 @@ esphome: | ||||
|           request_headers: | ||||
|             Content-Type: application/json | ||||
|           body: "Some data" | ||||
|       - http_request.post: | ||||
|           url: https://esphome.io | ||||
|           request_headers: | ||||
|             Content-Type: application/json | ||||
|           json: | ||||
|             key: value | ||||
|           capture_response: true | ||||
|           on_response: | ||||
|             then: | ||||
|               - logger.log: | ||||
|                   format: "Captured response status: %d, Body: %s" | ||||
|                   args: | ||||
|                     - response->status_code | ||||
|                     - body.c_str() | ||||
|  | ||||
| http_request: | ||||
|   useragent: esphome/tagreader | ||||
|   | ||||
| @@ -52,6 +52,19 @@ number: | ||||
|     widget: spinbox_id | ||||
|     id: lvgl_spinbox_number | ||||
|     name: LVGL Spinbox Number | ||||
|   - platform: template | ||||
|     id: test_brightness | ||||
|     name: "Test Brightness" | ||||
|     min_value: 0 | ||||
|     max_value: 255 | ||||
|     step: 1 | ||||
|     optimistic: true | ||||
|     # Test lambda in automation accessing x parameter directly | ||||
|     # This is a real-world pattern from user configs | ||||
|     on_value: | ||||
|       - lambda: !lambda |- | ||||
|           // Direct use of x parameter in automation | ||||
|           ESP_LOGD("test", "Brightness: %.0f", x); | ||||
|  | ||||
| light: | ||||
|   - platform: lvgl | ||||
| @@ -110,3 +123,21 @@ text: | ||||
|     platform: lvgl | ||||
|     widget: hello_label | ||||
|     mode: text | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: template | ||||
|     id: test_text_sensor | ||||
|     name: "Test Text Sensor" | ||||
|     # Test nested lambdas in LVGL actions can access automation parameters | ||||
|     on_value: | ||||
|       - lvgl.label.update: | ||||
|           id: hello_label | ||||
|           text: !lambda return x.c_str(); | ||||
|       - lvgl.label.update: | ||||
|           id: hello_label | ||||
|           text: !lambda |- | ||||
|             // Test complex lambda with conditionals accessing x parameter | ||||
|             if (x == "*") { | ||||
|               return "WILDCARD"; | ||||
|             } | ||||
|             return x.c_str(); | ||||
|   | ||||
| @@ -257,7 +257,30 @@ lvgl: | ||||
|             text: "Hello shiny day" | ||||
|             text_color: 0xFFFFFF | ||||
|             align: bottom_mid | ||||
|         - label: | ||||
|             id: setup_lambda_label | ||||
|             # Test lambda in widget property during setup (LvContext) | ||||
|             # Should NOT receive lv_component parameter | ||||
|             text: !lambda |- | ||||
|               char buf[32]; | ||||
|               snprintf(buf, sizeof(buf), "Setup: %d", 42); | ||||
|               return std::string(buf); | ||||
|             align: top_mid | ||||
|             text_font: space16 | ||||
|         - label: | ||||
|             id: chip_info_label | ||||
|             # Test complex setup lambda (real-world pattern) | ||||
|             # Should NOT receive lv_component parameter | ||||
|             text: !lambda |- | ||||
|               // Test conditional compilation and string formatting | ||||
|               char buf[64]; | ||||
|               #ifdef USE_ESP_IDF | ||||
|               snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR); | ||||
|               #else | ||||
|               snprintf(buf, sizeof(buf), "Arduino"); | ||||
|               #endif | ||||
|               return std::string(buf); | ||||
|             align: top_left | ||||
|         - obj: | ||||
|             align: center | ||||
|             arc_opa: COVER | ||||
|   | ||||
| @@ -56,6 +56,14 @@ binary_sensor: | ||||
|     register_type: read | ||||
|     address: 0x3200 | ||||
|     bitmask: 0x80 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_binary_sensor2 | ||||
|     name: Test Binary Sensor with Lambda | ||||
|     register_type: read | ||||
|     address: 0x3201 | ||||
|     lambda: |- | ||||
|       return x; | ||||
|  | ||||
| number: | ||||
|   - platform: modbus_controller | ||||
| @@ -65,6 +73,16 @@ number: | ||||
|     address: 0x9001 | ||||
|     value_type: U_WORD | ||||
|     multiply: 1.0 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_number2 | ||||
|     name: Test Number with Lambda | ||||
|     address: 0x9002 | ||||
|     value_type: U_WORD | ||||
|     lambda: |- | ||||
|       return x * 2.0; | ||||
|     write_lambda: |- | ||||
|       return x / 2.0; | ||||
|  | ||||
| output: | ||||
|   - platform: modbus_controller | ||||
| @@ -74,6 +92,14 @@ output: | ||||
|     register_type: holding | ||||
|     value_type: U_WORD | ||||
|     multiply: 1000 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_output2 | ||||
|     address: 2049 | ||||
|     register_type: holding | ||||
|     value_type: U_WORD | ||||
|     write_lambda: |- | ||||
|       return x * 100.0; | ||||
|  | ||||
| select: | ||||
|   - platform: modbus_controller | ||||
| @@ -87,6 +113,34 @@ select: | ||||
|       "One": 1 | ||||
|       "Two": 2 | ||||
|       "Three": 3 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_select2 | ||||
|     name: Test Select with Lambda | ||||
|     address: 1001 | ||||
|     value_type: U_WORD | ||||
|     optionsmap: | ||||
|       "Off": 0 | ||||
|       "On": 1 | ||||
|       "Two": 2 | ||||
|     lambda: |- | ||||
|       ESP_LOGD("Reg1001", "Received value %lld", x); | ||||
|       if (x > 1) { | ||||
|         return std::string("Two"); | ||||
|       } else if (x == 1) { | ||||
|         return std::string("On"); | ||||
|       } | ||||
|       return std::string("Off"); | ||||
|     write_lambda: |- | ||||
|       ESP_LOGD("Reg1001", "Set option to %s (%lld)", x.c_str(), value); | ||||
|       if (x == "On") { | ||||
|         return 1; | ||||
|       } | ||||
|       if (x == "Two") { | ||||
|         payload.push_back(0x0002); | ||||
|         return 0; | ||||
|       } | ||||
|       return value; | ||||
|  | ||||
| sensor: | ||||
|   - platform: modbus_controller | ||||
| @@ -97,6 +151,15 @@ sensor: | ||||
|     address: 0x9001 | ||||
|     unit_of_measurement: "AH" | ||||
|     value_type: U_WORD | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_sensor2 | ||||
|     name: Test Sensor with Lambda | ||||
|     register_type: holding | ||||
|     address: 0x9002 | ||||
|     value_type: U_WORD | ||||
|     lambda: |- | ||||
|       return x / 10.0; | ||||
|  | ||||
| switch: | ||||
|   - platform: modbus_controller | ||||
| @@ -106,6 +169,16 @@ switch: | ||||
|     register_type: coil | ||||
|     address: 0x15 | ||||
|     bitmask: 1 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_switch2 | ||||
|     name: Test Switch with Lambda | ||||
|     register_type: coil | ||||
|     address: 0x16 | ||||
|     lambda: |- | ||||
|       return !x; | ||||
|     write_lambda: |- | ||||
|       return !x; | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: modbus_controller | ||||
| @@ -117,3 +190,13 @@ text_sensor: | ||||
|     register_count: 3 | ||||
|     raw_encode: HEXBYTES | ||||
|     response_size: 6 | ||||
|   - platform: modbus_controller | ||||
|     modbus_controller_id: modbus_controller1 | ||||
|     id: modbus_text_sensor2 | ||||
|     name: Test Text Sensor with Lambda | ||||
|     register_type: holding | ||||
|     address: 0x9014 | ||||
|     register_count: 2 | ||||
|     response_size: 4 | ||||
|     lambda: |- | ||||
|       return "Modified: " + x; | ||||
|   | ||||
| @@ -48,6 +48,11 @@ on_drayton: | ||||
|     - logger.log: | ||||
|         format: "on_drayton: %u %u %u" | ||||
|         args: ["x.address", "x.channel", "x.command"] | ||||
| on_dyson: | ||||
|   then: | ||||
|     - logger.log: | ||||
|         format: "on_dyson: %u %u" | ||||
|         args: ["x.code", "x.index"] | ||||
| on_gobox: | ||||
|   then: | ||||
|     - logger.log: | ||||
|   | ||||
| @@ -6,6 +6,13 @@ button: | ||||
|       remote_transmitter.transmit_beo4: | ||||
|         source: 0x01 | ||||
|         command: 0x0C | ||||
|   - platform: template | ||||
|     name: Dyson fan up | ||||
|     id: dyson_fan_up | ||||
|     on_press: | ||||
|       remote_transmitter.transmit_dyson: | ||||
|         code: 0x1215 | ||||
|         index: 0x0 | ||||
|   - platform: template | ||||
|     name: JVC Off | ||||
|     id: living_room_lights_on | ||||
|   | ||||
| @@ -19,3 +19,41 @@ uart: | ||||
|  | ||||
| packet_transport: | ||||
|   - platform: uart | ||||
|  | ||||
| switch: | ||||
|   # Test uart switch with single state (array) | ||||
|   - platform: uart | ||||
|     name: "UART Switch Single Array" | ||||
|     uart_id: uart_uart | ||||
|     data: [0x01, 0x02, 0x03] | ||||
|   # Test uart switch with single state (string) | ||||
|   - platform: uart | ||||
|     name: "UART Switch Single String" | ||||
|     uart_id: uart_uart | ||||
|     data: "ON" | ||||
|   # Test uart switch with turn_on/turn_off (arrays) | ||||
|   - platform: uart | ||||
|     name: "UART Switch Dual Array" | ||||
|     uart_id: uart_uart | ||||
|     data: | ||||
|       turn_on: [0xA0, 0xA1, 0xA2] | ||||
|       turn_off: [0xB0, 0xB1, 0xB2] | ||||
|   # Test uart switch with turn_on/turn_off (strings) | ||||
|   - platform: uart | ||||
|     name: "UART Switch Dual String" | ||||
|     uart_id: uart_uart | ||||
|     data: | ||||
|       turn_on: "TURN_ON" | ||||
|       turn_off: "TURN_OFF" | ||||
|  | ||||
| button: | ||||
|   # Test uart button with array data | ||||
|   - platform: uart | ||||
|     name: "UART Button Array" | ||||
|     uart_id: uart_uart | ||||
|     data: [0xFF, 0xEE, 0xDD] | ||||
|   # Test uart button with string data | ||||
|   - platform: uart | ||||
|     name: "UART Button String" | ||||
|     uart_id: uart_uart | ||||
|     data: "BUTTON_PRESS" | ||||
|   | ||||
| @@ -13,3 +13,21 @@ uart: | ||||
|     rx_buffer_size: 512 | ||||
|     parity: EVEN | ||||
|     stop_bits: 2 | ||||
|  | ||||
| switch: | ||||
|   - platform: uart | ||||
|     name: "UART Switch Array" | ||||
|     uart_id: uart_uart | ||||
|     data: [0x01, 0x02, 0x03] | ||||
|   - platform: uart | ||||
|     name: "UART Switch Dual" | ||||
|     uart_id: uart_uart | ||||
|     data: | ||||
|       turn_on: [0xA0, 0xA1] | ||||
|       turn_off: [0xB0, 0xB1] | ||||
|  | ||||
| button: | ||||
|   - platform: uart | ||||
|     name: "UART Button" | ||||
|     uart_id: uart_uart | ||||
|     data: [0xFF, 0xEE] | ||||
|   | ||||
| @@ -41,6 +41,17 @@ select: | ||||
|       - ""  # Empty string at the end | ||||
|     initial_option: "Choice X" | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Select Initial Option Test" | ||||
|     id: select_initial_option_test | ||||
|     optimistic: true | ||||
|     options: | ||||
|       - "First" | ||||
|       - "Second" | ||||
|       - "Third" | ||||
|       - "Fourth" | ||||
|     initial_option: "Third"  # Test non-default initial option | ||||
|  | ||||
| # Add a sensor to ensure we have other entities in the list | ||||
| sensor: | ||||
|   - platform: template | ||||
|   | ||||
| @@ -44,6 +44,7 @@ class InitialStateHelper: | ||||
|         helper = InitialStateHelper(entities) | ||||
|         client.subscribe_states(helper.on_state_wrapper(user_callback)) | ||||
|         await helper.wait_for_initial_states() | ||||
|         # Access initial states via helper.initial_states[key] | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, entities: list[EntityInfo]) -> None: | ||||
| @@ -63,6 +64,8 @@ class InitialStateHelper: | ||||
|         self._entities_by_id = { | ||||
|             (entity.device_id, entity.key): entity for entity in entities | ||||
|         } | ||||
|         # Store initial states by key for test access | ||||
|         self.initial_states: dict[int, EntityState] = {} | ||||
|  | ||||
|         # Log all entities | ||||
|         _LOGGER.debug( | ||||
| @@ -127,6 +130,9 @@ class InitialStateHelper: | ||||
|  | ||||
|             # If this entity is waiting for initial state | ||||
|             if entity_id in self._wait_initial_states: | ||||
|                 # Store the initial state for test access | ||||
|                 self.initial_states[state.key] = state | ||||
|  | ||||
|                 # Remove from waiting set | ||||
|                 self._wait_initial_states.discard(entity_id) | ||||
|  | ||||
|   | ||||
| @@ -2,12 +2,11 @@ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
|  | ||||
| import aioesphomeapi | ||||
| from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState | ||||
| from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset | ||||
| import pytest | ||||
|  | ||||
| from .state_utils import InitialStateHelper | ||||
| from .types import APIClientConnectedFactory, RunCompiledFunction | ||||
|  | ||||
|  | ||||
| @@ -18,26 +17,27 @@ async def test_host_mode_climate_basic_state( | ||||
|     api_client_connected: APIClientConnectedFactory, | ||||
| ) -> None: | ||||
|     """Test basic climate state reporting.""" | ||||
|     loop = asyncio.get_running_loop() | ||||
|     async with run_compiled(yaml_config), api_client_connected() as client: | ||||
|         states: dict[int, EntityState] = {} | ||||
|         climate_future: asyncio.Future[EntityState] = loop.create_future() | ||||
|         # Get entities and set up state synchronization | ||||
|         entities, services = await client.list_entities_services() | ||||
|         initial_state_helper = InitialStateHelper(entities) | ||||
|         climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] | ||||
|         assert len(climate_infos) >= 1, "Expected at least 1 climate entity" | ||||
|  | ||||
|         def on_state(state: EntityState) -> None: | ||||
|             states[state.key] = state | ||||
|             if ( | ||||
|                 isinstance(state, aioesphomeapi.ClimateState) | ||||
|                 and not climate_future.done() | ||||
|             ): | ||||
|                 climate_future.set_result(state) | ||||
|  | ||||
|         client.subscribe_states(on_state) | ||||
|         # Subscribe with the wrapper (no-op callback since we just want initial states) | ||||
|         client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None)) | ||||
|  | ||||
|         # Wait for all initial states to be broadcast | ||||
|         try: | ||||
|             climate_state = await asyncio.wait_for(climate_future, timeout=5.0) | ||||
|             await initial_state_helper.wait_for_initial_states() | ||||
|         except TimeoutError: | ||||
|             pytest.fail("Climate state not received within 5 seconds") | ||||
|             pytest.fail("Timeout waiting for initial states") | ||||
|  | ||||
|         # Get the climate entity and its initial state | ||||
|         test_climate = climate_infos[0] | ||||
|         climate_state = initial_state_helper.initial_states.get(test_climate.key) | ||||
|  | ||||
|         assert climate_state is not None, "Climate initial state not found" | ||||
|         assert isinstance(climate_state, aioesphomeapi.ClimateState) | ||||
|         assert climate_state.mode == ClimateMode.OFF | ||||
|         assert climate_state.action == ClimateAction.OFF | ||||
|   | ||||
| @@ -36,8 +36,8 @@ async def test_host_mode_empty_string_options( | ||||
|  | ||||
|         # Find our select entities | ||||
|         select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] | ||||
|         assert len(select_entities) == 3, ( | ||||
|             f"Expected 3 select entities, got {len(select_entities)}" | ||||
|         assert len(select_entities) == 4, ( | ||||
|             f"Expected 4 select entities, got {len(select_entities)}" | ||||
|         ) | ||||
|  | ||||
|         # Verify each select entity by name and check their options | ||||
| @@ -71,6 +71,15 @@ async def test_host_mode_empty_string_options( | ||||
|         assert empty_last.options[2] == "Choice Z" | ||||
|         assert empty_last.options[3] == ""  # Empty string at end | ||||
|  | ||||
|         # Check "Select Initial Option Test" - verify non-default initial option | ||||
|         assert "Select Initial Option Test" in selects_by_name | ||||
|         initial_option_test = selects_by_name["Select Initial Option Test"] | ||||
|         assert len(initial_option_test.options) == 4 | ||||
|         assert initial_option_test.options[0] == "First" | ||||
|         assert initial_option_test.options[1] == "Second" | ||||
|         assert initial_option_test.options[2] == "Third" | ||||
|         assert initial_option_test.options[3] == "Fourth" | ||||
|  | ||||
|         # If we got here without protobuf decoding errors, the fix is working | ||||
|         # The bug would have caused "Invalid protobuf message" errors with trailing bytes | ||||
|  | ||||
| @@ -78,7 +87,12 @@ async def test_host_mode_empty_string_options( | ||||
|         # This ensures empty strings work properly in state messages too | ||||
|         states: dict[int, EntityState] = {} | ||||
|         states_received_future: asyncio.Future[None] = loop.create_future() | ||||
|         expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key} | ||||
|         expected_select_keys = { | ||||
|             empty_first.key, | ||||
|             empty_middle.key, | ||||
|             empty_last.key, | ||||
|             initial_option_test.key, | ||||
|         } | ||||
|         received_select_keys = set() | ||||
|  | ||||
|         def on_state(state: EntityState) -> None: | ||||
| @@ -109,6 +123,14 @@ async def test_host_mode_empty_string_options( | ||||
|         assert empty_first.key in states | ||||
|         assert empty_middle.key in states | ||||
|         assert empty_last.key in states | ||||
|         assert initial_option_test.key in states | ||||
|  | ||||
|         # Verify the initial option is set correctly to "Third" (not the default "First") | ||||
|         initial_state = states[initial_option_test.key] | ||||
|         assert initial_state.state == "Third", ( | ||||
|             f"Expected initial state 'Third' but got '{initial_state.state}' - " | ||||
|             f"initial_option not correctly applied" | ||||
|         ) | ||||
|  | ||||
|         # The main test is that we got here without protobuf errors | ||||
|         # The select entities with empty string options were properly encoded | ||||
|   | ||||
| @@ -849,39 +849,47 @@ def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) -> | ||||
|     assert result["should_run"] == "false" | ||||
|  | ||||
|  | ||||
| def test_detect_memory_impact_config_skips_base_bus_components(tmp_path: Path) -> None: | ||||
|     """Test that base bus components (i2c, spi, uart) are skipped.""" | ||||
| def test_detect_memory_impact_config_includes_base_bus_components( | ||||
|     tmp_path: Path, | ||||
| ) -> None: | ||||
|     """Test that base bus components (i2c, spi, uart) are included when directly changed. | ||||
|  | ||||
|     Base bus components should be analyzed for memory impact when they are directly | ||||
|     changed, even though they are often used as dependencies. This ensures that | ||||
|     optimizations to base components (like using move semantics or initializer_list) | ||||
|     are properly measured. | ||||
|     """ | ||||
|     # Create test directory structure | ||||
|     tests_dir = tmp_path / "tests" / "components" | ||||
|  | ||||
|     # i2c component (should be skipped as it's a base bus component) | ||||
|     i2c_dir = tests_dir / "i2c" | ||||
|     i2c_dir.mkdir(parents=True) | ||||
|     (i2c_dir / "test.esp32-idf.yaml").write_text("test: i2c") | ||||
|     # uart component (base bus component that should be included) | ||||
|     uart_dir = tests_dir / "uart" | ||||
|     uart_dir.mkdir(parents=True) | ||||
|     (uart_dir / "test.esp32-idf.yaml").write_text("test: uart") | ||||
|  | ||||
|     # wifi component (should not be skipped) | ||||
|     # wifi component (regular component) | ||||
|     wifi_dir = tests_dir / "wifi" | ||||
|     wifi_dir.mkdir(parents=True) | ||||
|     (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") | ||||
|  | ||||
|     # Mock changed_files to return both i2c and wifi | ||||
|     # Mock changed_files to return both uart and wifi | ||||
|     with ( | ||||
|         patch.object(determine_jobs, "root_path", str(tmp_path)), | ||||
|         patch.object(helpers, "root_path", str(tmp_path)), | ||||
|         patch.object(determine_jobs, "changed_files") as mock_changed_files, | ||||
|     ): | ||||
|         mock_changed_files.return_value = [ | ||||
|             "esphome/components/i2c/i2c.cpp", | ||||
|             "esphome/components/uart/automation.h",  # Header file with inline code | ||||
|             "esphome/components/wifi/wifi.cpp", | ||||
|         ] | ||||
|         determine_jobs._component_has_tests.cache_clear() | ||||
|  | ||||
|         result = determine_jobs.detect_memory_impact_config() | ||||
|  | ||||
|     # Should only include wifi, not i2c | ||||
|     # Should include both uart and wifi | ||||
|     assert result["should_run"] == "true" | ||||
|     assert result["components"] == ["wifi"] | ||||
|     assert "i2c" not in result["components"] | ||||
|     assert set(result["components"]) == {"uart", "wifi"} | ||||
|     assert result["platform"] == "esp32-idf"  # Common platform | ||||
|  | ||||
|  | ||||
| def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: | ||||
|   | ||||
| @@ -1065,3 +1065,39 @@ def test_parse_list_components_output(output: str, expected: list[str]) -> None: | ||||
|     """Test parse_list_components_output function.""" | ||||
|     result = helpers.parse_list_components_output(output) | ||||
|     assert result == expected | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     ("file_path", "expected_component"), | ||||
|     [ | ||||
|         # Component files | ||||
|         ("esphome/components/wifi/wifi.cpp", "wifi"), | ||||
|         ("esphome/components/uart/uart.h", "uart"), | ||||
|         ("esphome/components/api/api_server.cpp", "api"), | ||||
|         ("esphome/components/sensor/sensor.cpp", "sensor"), | ||||
|         # Test files | ||||
|         ("tests/components/uart/test.esp32-idf.yaml", "uart"), | ||||
|         ("tests/components/wifi/test.esp8266-ard.yaml", "wifi"), | ||||
|         ("tests/components/sensor/test.esp32-idf.yaml", "sensor"), | ||||
|         ("tests/components/api/test_api.cpp", "api"), | ||||
|         ("tests/components/uart/common.h", "uart"), | ||||
|         # Non-component files | ||||
|         ("esphome/core/component.cpp", None), | ||||
|         ("esphome/core/helpers.h", None), | ||||
|         ("tests/integration/test_api.py", None), | ||||
|         ("tests/unit_tests/test_helpers.py", None), | ||||
|         ("README.md", None), | ||||
|         ("script/helpers.py", None), | ||||
|         # Edge cases | ||||
|         ("esphome/components/", None),  # No component name | ||||
|         ("tests/components/", None),  # No component name | ||||
|         ("esphome/components", None),  # No trailing slash | ||||
|         ("tests/components", None),  # No trailing slash | ||||
|     ], | ||||
| ) | ||||
| def test_get_component_from_path( | ||||
|     file_path: str, expected_component: str | None | ||||
| ) -> None: | ||||
|     """Test extraction of component names from file paths.""" | ||||
|     result = helpers.get_component_from_path(file_path) | ||||
|     assert result == expected_component | ||||
|   | ||||
		Reference in New Issue
	
	Block a user