mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 23:21:54 +00:00 
			
		
		
		
	preen
This commit is contained in:
		| @@ -9,6 +9,7 @@ from esphome.const import ( | |||||||
|     CONF_COOL_DEADBAND, |     CONF_COOL_DEADBAND, | ||||||
|     CONF_COOL_MODE, |     CONF_COOL_MODE, | ||||||
|     CONF_COOL_OVERRUN, |     CONF_COOL_OVERRUN, | ||||||
|  |     CONF_CUSTOM_FAN_MODES, | ||||||
|     CONF_DEFAULT_MODE, |     CONF_DEFAULT_MODE, | ||||||
|     CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, |     CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, | ||||||
|     CONF_DEFAULT_TARGET_TEMPERATURE_LOW, |     CONF_DEFAULT_TARGET_TEMPERATURE_LOW, | ||||||
| @@ -658,6 +659,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), |             cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), | ||||||
|  |             cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(cv.string_strict), | ||||||
|             cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from, |             cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from, | ||||||
|             cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( |             cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( | ||||||
|                 single=True |                 single=True | ||||||
| @@ -1008,3 +1010,21 @@ async def to_code(config): | |||||||
|         await automation.build_automation( |         await automation.build_automation( | ||||||
|             var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] |             var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     # Collect custom preset names from preset map (non-standard preset names only) | ||||||
|  |     custom_preset_names = [ | ||||||
|  |         preset_config[CONF_NAME] | ||||||
|  |         for preset_config in config.get(CONF_PRESET, []) | ||||||
|  |         if preset_config[CONF_NAME].upper() not in climate.CLIMATE_PRESETS | ||||||
|  |     ] | ||||||
|  |     if custom_preset_names: | ||||||
|  |         cg.add(var.set_custom_presets(custom_preset_names)) | ||||||
|  |  | ||||||
|  |     # Collect custom fan modes (filter out standard enum fan modes) | ||||||
|  |     custom_fan_modes = [ | ||||||
|  |         mode | ||||||
|  |         for mode in config.get(CONF_CUSTOM_FAN_MODES, []) | ||||||
|  |         if mode.upper() not in climate.CLIMATE_FAN_MODES | ||||||
|  |     ] | ||||||
|  |     if custom_fan_modes: | ||||||
|  |         cg.add(var.set_custom_fan_modes(custom_fan_modes)) | ||||||
|   | |||||||
| @@ -0,0 +1,44 @@ | |||||||
|  | esphome: | ||||||
|  |   name: climate-custom-modes-test | ||||||
|  | host: | ||||||
|  | api: | ||||||
|  | logger: | ||||||
|  |  | ||||||
|  | sensor: | ||||||
|  |   - platform: template | ||||||
|  |     id: thermostat_sensor | ||||||
|  |     lambda: "return 22.0;" | ||||||
|  |  | ||||||
|  | climate: | ||||||
|  |   - platform: thermostat | ||||||
|  |     id: test_thermostat | ||||||
|  |     name: Test Thermostat Custom Modes | ||||||
|  |     sensor: thermostat_sensor | ||||||
|  |     preset: | ||||||
|  |       - name: Away | ||||||
|  |         default_target_temperature_low: 16°C | ||||||
|  |         default_target_temperature_high: 20°C | ||||||
|  |       - name: Eco Plus | ||||||
|  |         default_target_temperature_low: 18°C | ||||||
|  |         default_target_temperature_high: 22°C | ||||||
|  |       - name: Super Saver | ||||||
|  |         default_target_temperature_low: 20°C | ||||||
|  |         default_target_temperature_high: 24°C | ||||||
|  |       - name: Vacation Mode | ||||||
|  |         default_target_temperature_low: 15°C | ||||||
|  |         default_target_temperature_high: 18°C | ||||||
|  |     custom_fan_modes: | ||||||
|  |       - "Turbo" | ||||||
|  |       - "Silent" | ||||||
|  |       - "Sleep Mode" | ||||||
|  |     idle_action: | ||||||
|  |       - logger.log: idle_action | ||||||
|  |     cool_action: | ||||||
|  |       - logger.log: cool_action | ||||||
|  |     heat_action: | ||||||
|  |       - logger.log: heat_action | ||||||
|  |     min_cooling_off_time: 10s | ||||||
|  |     min_cooling_run_time: 10s | ||||||
|  |     min_heating_off_time: 10s | ||||||
|  |     min_heating_run_time: 10s | ||||||
|  |     min_idle_time: 10s | ||||||
							
								
								
									
										51
									
								
								tests/integration/test_climate_custom_modes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/integration/test_climate_custom_modes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | """Integration test for climate custom fan modes and presets.""" | ||||||
|  |  | ||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from aioesphomeapi import ClimateInfo, ClimatePreset | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_climate_custom_fan_modes_and_presets( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that custom fan modes and presets are properly exposed via API.""" | ||||||
|  |     async with run_compiled(yaml_config), api_client_connected() as client: | ||||||
|  |         # Get entities and services | ||||||
|  |         entities, services = await client.list_entities_services() | ||||||
|  |         climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] | ||||||
|  |         assert len(climate_infos) == 1, "Expected exactly 1 climate entity" | ||||||
|  |  | ||||||
|  |         test_climate = climate_infos[0] | ||||||
|  |  | ||||||
|  |         # Verify custom fan modes are exposed | ||||||
|  |         custom_fan_modes = test_climate.supported_custom_fan_modes | ||||||
|  |         assert len(custom_fan_modes) == 3, ( | ||||||
|  |             f"Expected 3 custom fan modes, got {len(custom_fan_modes)}" | ||||||
|  |         ) | ||||||
|  |         assert "Turbo" in custom_fan_modes, "Expected 'Turbo' in custom fan modes" | ||||||
|  |         assert "Silent" in custom_fan_modes, "Expected 'Silent' in custom fan modes" | ||||||
|  |         assert "Sleep Mode" in custom_fan_modes, ( | ||||||
|  |             "Expected 'Sleep Mode' in custom fan modes" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify enum presets are exposed (from preset: config map) | ||||||
|  |         assert ClimatePreset.AWAY in test_climate.supported_presets, ( | ||||||
|  |             "Expected AWAY in enum presets" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify custom string presets are exposed (non-standard preset names from preset map) | ||||||
|  |         custom_presets = test_climate.supported_custom_presets | ||||||
|  |         assert len(custom_presets) == 3, ( | ||||||
|  |             f"Expected 3 custom presets, got {len(custom_presets)}: {custom_presets}" | ||||||
|  |         ) | ||||||
|  |         assert "Eco Plus" in custom_presets, "Expected 'Eco Plus' in custom presets" | ||||||
|  |         assert "Comfort" in custom_presets, "Expected 'Comfort' in custom presets" | ||||||
|  |         assert "Vacation Mode" in custom_presets, ( | ||||||
|  |             "Expected 'Vacation Mode' in custom presets" | ||||||
|  |         ) | ||||||
		Reference in New Issue
	
	Block a user