From 1817522107617888f683aa08e7cb14fdcc2b287c Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Mon, 16 Mar 2026 14:06:23 -0700 Subject: [PATCH] Clean up SmartTub integration and tests (#165517) Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Joost Lekkerkerker --- homeassistant/components/smarttub/__init__.py | 3 +- tests/components/smarttub/conftest.py | 3 +- tests/components/smarttub/test_climate.py | 56 +++++++----- tests/components/smarttub/test_config_flow.py | 86 ++++++++++-------- tests/components/smarttub/test_init.py | 34 +++---- tests/components/smarttub/test_switch.py | 90 ++++++++++++------- 6 files changed, 151 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index 178fd9a70e2..2585a3554d4 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -19,8 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartTubConfigEntry) -> controller = SmartTubController(hass) - if not await controller.async_setup_entry(entry): - return False + await controller.async_setup_entry(entry) entry.runtime_data = controller diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index f7677100aad..6bc0e768145 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -21,12 +21,13 @@ def config_data() -> dict[str, Any]: @pytest.fixture -def config_entry(config_data: dict[str, Any]) -> MockConfigEntry: +def config_entry(config_data: dict[str, Any], account) -> MockConfigEntry: """Create a mock config entry.""" return MockConfigEntry( domain=DOMAIN, data=config_data, options={}, + unique_id=account.id, ) diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index e4e73b8b131..50c6ce233c9 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -5,7 +5,6 @@ import smarttub from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, - ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, @@ -14,7 +13,6 @@ from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, PRESET_ECO, PRESET_NONE, - SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ClimateEntityFeature, @@ -32,25 +30,16 @@ from homeassistant.core import HomeAssistant from . import trigger_update -async def test_thermostat_update( +async def test_thermostat_state( spa, spa_state, setup_entry, hass: HomeAssistant ) -> None: - """Test the thermostat entity.""" - + """Test the thermostat entity initial state and attributes.""" entity_id = f"climate.{spa.brand}_{spa.model}_thermostat" state = hass.states.get(entity_id) assert state - - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING - - spa_state.heater = "OFF" - await trigger_update(hass) - state = hass.states.get(entity_id) - - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE - - assert set(state.attributes[ATTR_HVAC_MODES]) == {HVACMode.HEAT} assert state.state == HVACMode.HEAT + assert set(state.attributes[ATTR_HVAC_MODES]) == {HVACMode.HEAT} + assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING assert ( state.attributes[ATTR_SUPPORTED_FEATURES] == ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE @@ -60,7 +49,28 @@ async def test_thermostat_update( assert state.attributes[ATTR_MAX_TEMP] == DEFAULT_MAX_TEMP assert state.attributes[ATTR_MIN_TEMP] == DEFAULT_MIN_TEMP assert state.attributes[ATTR_PRESET_MODES] == ["none", "eco", "day", "ready"] + assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_NONE + +async def test_thermostat_hvac_action_update( + spa, spa_state, setup_entry, hass: HomeAssistant +) -> None: + """Test the thermostat HVAC action transitions from heating to idle.""" + entity_id = f"climate.{spa.brand}_{spa.model}_thermostat" + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING + + spa_state.heater = "OFF" + await trigger_update(hass) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE + + +async def test_thermostat_set_temperature( + spa, setup_entry, hass: HomeAssistant +) -> None: + """Test setting the target temperature.""" + entity_id = f"climate.{spa.brand}_{spa.model}_thermostat" await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -69,15 +79,12 @@ async def test_thermostat_update( ) spa.set_temperature.assert_called_with(37) - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.HEAT}, - blocking=True, - ) - # does nothing - assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_NONE +async def test_thermostat_set_preset_mode( + spa, spa_state, setup_entry, hass: HomeAssistant +) -> None: + """Test setting a preset mode updates state correctly.""" + entity_id = f"climate.{spa.brand}_{spa.model}_thermostat" await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -91,6 +98,9 @@ async def test_thermostat_update( state = hass.states.get(entity_id) assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO + +async def test_thermostat_api_error(spa, setup_entry, hass: HomeAssistant) -> None: + """Test that an API error during update does not raise.""" spa.get_status_full.side_effect = smarttub.APIError await trigger_update(hass) # should not fail diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index 5832841641c..37d1b4c8197 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch +import pytest from smarttub import LoginFailed from homeassistant import config_entries @@ -13,35 +14,43 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" +@pytest.fixture +def mock_setup_entry(): + """Mock the integration setup.""" + with patch( + "homeassistant.components.smarttub.async_setup_entry", + return_value=True, + ) as mock: + yield mock + + +async def test_user_flow(hass: HomeAssistant, mock_setup_entry, account) -> None: + """Test the user config flow creates an entry with correct data.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.smarttub.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, + ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-email" - assert result["data"] == { - CONF_EMAIL: "test-email", - CONF_PASSWORD: "test-password", - } - await hass.async_block_till_done() - mock_setup_entry.assert_called_once() + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-email" + assert result["data"] == { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + } + assert result["result"].unique_id == account.id + mock_setup_entry.assert_called_once() -async def test_form_invalid_auth(hass: HomeAssistant, smarttub_api) -> None: - """Test we handle invalid auth.""" +async def test_form_invalid_auth( + hass: HomeAssistant, smarttub_api, mock_setup_entry +) -> None: + """Test we handle invalid auth and can recover.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -56,17 +65,21 @@ async def test_form_invalid_auth(hass: HomeAssistant, smarttub_api) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} + smarttub_api.login.side_effect = None -async def test_reauth_success(hass: HomeAssistant, smarttub_api, account) -> None: - """Test reauthentication flow.""" - mock_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, - unique_id=account.id, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - mock_entry.add_to_hass(hass) - result = await mock_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_reauth_success(hass: HomeAssistant, smarttub_api, config_entry) -> None: + """Test reauthentication flow.""" + config_entry.add_to_hass(hass) + + result = await config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -77,18 +90,15 @@ async def test_reauth_success(hass: HomeAssistant, smarttub_api, account) -> Non assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert mock_entry.data[CONF_EMAIL] == "test-email3" - assert mock_entry.data[CONF_PASSWORD] == "test-password3" + assert config_entry.data[CONF_EMAIL] == "test-email3" + assert config_entry.data[CONF_PASSWORD] == "test-password3" -async def test_reauth_wrong_account(hass: HomeAssistant, smarttub_api, account) -> None: +async def test_reauth_wrong_account( + hass: HomeAssistant, smarttub_api, account, config_entry +) -> None: """Test reauthentication flow if the user enters credentials for a different already-configured account.""" - mock_entry1 = MockConfigEntry( - domain=DOMAIN, - data={CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"}, - unique_id=account.id, - ) - mock_entry1.add_to_hass(hass) + config_entry.add_to_hass(hass) mock_entry2 = MockConfigEntry( domain=DOMAIN, @@ -98,7 +108,7 @@ async def test_reauth_wrong_account(hass: HomeAssistant, smarttub_api, account) mock_entry2.add_to_hass(hass) # we try to reauth account #2, and the user successfully authenticates to account #1 - account.id = mock_entry1.unique_id + account.id = config_entry.unique_id result = await mock_entry2.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index ff27820fca1..5679348c7e7 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -1,13 +1,10 @@ """Test smarttub setup process.""" -from unittest.mock import patch - from smarttub import LoginFailed from homeassistant.components.smarttub.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component async def test_setup_with_no_config( @@ -39,34 +36,23 @@ async def test_setup_auth_failed( smarttub_api.login.side_effect = LoginFailed config_entry.add_to_hass(hass) - with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.SETUP_ERROR - mock_flow_init.assert_called_with( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "entry_id": config_entry.entry_id, - "unique_id": config_entry.unique_id, - "title_placeholders": {"name": config_entry.title}, - }, - data=config_entry.data, - ) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.SETUP_ERROR -async def test_config_passed_to_config_entry( - hass: HomeAssistant, config_entry, config_data -) -> None: - """Test that configured options are loaded via config entry.""" - config_entry.add_to_hass(hass) - assert await async_setup_component(hass, DOMAIN, config_data) + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert len(flows) == 1 + assert flows[0]["context"]["source"] == SOURCE_REAUTH async def test_unload_entry(hass: HomeAssistant, config_entry) -> None: """Test being able to unload an entry.""" config_entry.add_to_hass(hass) - assert await async_setup_component(hass, DOMAIN, {}) is True + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/smarttub/test_switch.py b/tests/components/smarttub/test_switch.py index 42fd3d45d88..7d071d3a335 100644 --- a/tests/components/smarttub/test_switch.py +++ b/tests/components/smarttub/test_switch.py @@ -7,49 +7,73 @@ from homeassistant.core import HomeAssistant @pytest.mark.parametrize( - ("pump_id", "entity_suffix", "pump_state"), + ("pump_id", "entity_suffix", "expected_state"), [ - ("CP", "circulation_pump", "off"), - ("P1", "jet_p1", "off"), - ("P2", "jet_p2", "on"), + ("CP", "circulation_pump", STATE_OFF), + ("P1", "jet_p1", STATE_OFF), + ("P2", "jet_p2", STATE_ON), ], ) -async def test_pumps( - spa, setup_entry, hass: HomeAssistant, pump_id, pump_state, entity_suffix +async def test_pump_state( + spa, setup_entry, hass: HomeAssistant, pump_id, entity_suffix, expected_state ) -> None: - """Test pump entities.""" - - status = await spa.get_status_full() - pump = next(pump for pump in status.pumps if pump.id == pump_id) - + """Test pump entity initial state.""" entity_id = f"switch.{spa.brand}_{spa.model}_{entity_suffix}" state = hass.states.get(entity_id) assert state is not None - assert state.state == pump_state + assert state.state == expected_state + + +@pytest.mark.parametrize( + ("pump_id", "entity_suffix"), + [ + ("CP", "circulation_pump"), + ("P1", "jet_p1"), + ("P2", "jet_p2"), + ], +) +async def test_pump_toggle( + spa, setup_entry, hass: HomeAssistant, pump_id, entity_suffix +) -> None: + """Test toggling a pump.""" + status = await spa.get_status_full() + pump = next(pump for pump in status.pumps if pump.id == pump_id) + entity_id = f"switch.{spa.brand}_{spa.model}_{entity_suffix}" await hass.services.async_call( - "switch", - "toggle", - {"entity_id": entity_id}, - blocking=True, + "switch", "toggle", {"entity_id": entity_id}, blocking=True ) pump.toggle.assert_called() - if state.state == STATE_OFF: - await hass.services.async_call( - "switch", - "turn_on", - {"entity_id": entity_id}, - blocking=True, - ) - pump.toggle.assert_called() - else: - assert state.state == STATE_ON - await hass.services.async_call( - "switch", - "turn_off", - {"entity_id": entity_id}, - blocking=True, - ) - pump.toggle.assert_called() +@pytest.mark.parametrize( + ("pump_id", "entity_suffix"), + [ + ("CP", "circulation_pump"), + ("P1", "jet_p1"), + ], +) +async def test_pump_turn_on( + spa, setup_entry, hass: HomeAssistant, pump_id, entity_suffix +) -> None: + """Test turning on an off pump toggles it.""" + status = await spa.get_status_full() + pump = next(pump for pump in status.pumps if pump.id == pump_id) + entity_id = f"switch.{spa.brand}_{spa.model}_{entity_suffix}" + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + pump.toggle.assert_called() + + +async def test_pump_turn_off(spa, setup_entry, hass: HomeAssistant) -> None: + """Test turning off an on pump toggles it.""" + status = await spa.get_status_full() + pump = next(pump for pump in status.pumps if pump.id == "P2") + entity_id = f"switch.{spa.brand}_{spa.model}_jet_p2" + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + pump.toggle.assert_called()