"""Test the Whirlpool Sensor domain.""" from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion from whirlpool.washerdryer import MachineState from homeassistant.components.whirlpool.sensor import SCAN_INTERVAL from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import as_timestamp, utc_from_timestamp, utcnow from . import init_integration, snapshot_whirlpool_entities from tests.common import async_fire_time_changed, mock_restore_cache_with_extra_data WASHER_ENTITY_ID_BASE = "sensor.washer" DRYER_ENTITY_ID_BASE = "sensor.dryer" async def trigger_attr_callback( hass: HomeAssistant, mock_api_instance: MagicMock ) -> None: """Simulate an update trigger from the API.""" for call in mock_api_instance.register_attr_callback.call_args_list: update_ha_state_cb = call[0][0] update_ha_state_cb() await hass.async_block_till_done() # Freeze time for WasherDryerTimeSensor @pytest.mark.freeze_time("2025-05-04 12:00:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_all_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" await init_integration(hass) snapshot_whirlpool_entities(hass, entity_registry, snapshot, Platform.SENSOR) @pytest.mark.parametrize( ("entity_id", "mock_fixture"), [ ("sensor.washer_end_time", "mock_washer_api"), ("sensor.dryer_end_time", "mock_dryer_api"), ], ) @pytest.mark.freeze_time("2022-11-30 00:00:00") async def test_washer_dryer_time_sensor( hass: HomeAssistant, entity_id: str, mock_fixture: str, request: pytest.FixtureRequest, freezer: FrozenDateTimeFactory, ) -> None: """Test Washer/Dryer end time sensors.""" now = utcnow() restored_datetime: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, UTC) mock_restore_cache_with_extra_data( hass, [ ( State(entity_id, "1"), {"native_value": restored_datetime, "native_unit_of_measurement": None}, ) ], ) mock_instance = request.getfixturevalue(mock_fixture) mock_instance.get_machine_state.return_value = MachineState.Pause await init_integration(hass) # Test restored state. state = hass.states.get(entity_id) assert state.state == restored_datetime.isoformat() # Test no time change because the machine is not running. await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert state.state == restored_datetime.isoformat() # Test new time when machine starts a cycle. mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle mock_instance.get_time_remaining.return_value = 60 await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) expected_time = (now + timedelta(seconds=60)).isoformat() assert state.state == expected_time # Test no state change for < 60 seconds elapsed time. mock_instance.get_time_remaining.return_value = 65 await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert state.state == expected_time # Test timestamp change for > 60 seconds. mock_instance.get_time_remaining.return_value = 125 await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert ( state.state == utc_from_timestamp(as_timestamp(expected_time) + 65).isoformat() ) # Test that periodic updates call the API to fetch data mock_instance.fetch_data.reset_mock() freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() mock_instance.fetch_data.assert_called_once() @pytest.mark.parametrize( ("entity_id", "mock_fixture"), [ ("sensor.washer_end_time", "mock_washer_api"), ("sensor.dryer_end_time", "mock_dryer_api"), ], ) @pytest.mark.freeze_time("2022-11-30 00:00:00") async def test_washer_dryer_time_sensor_no_restore( hass: HomeAssistant, entity_id: str, mock_fixture: str, request: pytest.FixtureRequest, ) -> None: """Test Washer/Dryer end time sensors without state restore.""" now = utcnow() mock_instance = request.getfixturevalue(mock_fixture) mock_instance.get_machine_state.return_value = MachineState.Pause await init_integration(hass) state = hass.states.get(entity_id) assert state.state == STATE_UNKNOWN # Test no change because the machine is paused. await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert state.state == STATE_UNKNOWN # Test new time when machine starts a cycle. mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle mock_instance.get_time_remaining.return_value = 60 await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) expected_time = (now + timedelta(seconds=60)).isoformat() assert state.state == expected_time @pytest.mark.parametrize( ("entity_id", "mock_fixture"), [ ("sensor.washer_state", "mock_washer_api"), ("sensor.dryer_state", "mock_dryer_api"), ], ) @pytest.mark.parametrize( ("machine_state", "expected_state"), [ (MachineState.Standby, "standby"), (MachineState.Setting, "setting"), (MachineState.DelayCountdownMode, "delay_countdown"), (MachineState.DelayPause, "delay_paused"), (MachineState.SmartDelay, "smart_delay"), (MachineState.SmartGridPause, "smart_grid_pause"), (MachineState.Pause, "pause"), (MachineState.RunningMainCycle, "running_maincycle"), (MachineState.RunningPostCycle, "running_postcycle"), (MachineState.Exceptions, "exception"), (MachineState.Complete, "complete"), (MachineState.PowerFailure, "power_failure"), (MachineState.ServiceDiagnostic, "service_diagnostic_mode"), (MachineState.FactoryDiagnostic, "factory_diagnostic_mode"), (MachineState.LifeTest, "life_test"), (MachineState.CustomerFocusMode, "customer_focus_mode"), (MachineState.DemoMode, "demo_mode"), (MachineState.HardStopOrError, "hard_stop_or_error"), (MachineState.SystemInit, "system_initialize"), ], ) async def test_washer_dryer_machine_states( hass: HomeAssistant, entity_id: str, mock_fixture: str, machine_state: MachineState, expected_state: str, request: pytest.FixtureRequest, ) -> None: """Test Washer/Dryer machine states.""" mock_instance = request.getfixturevalue(mock_fixture) await init_integration(hass) mock_instance.get_machine_state.return_value = machine_state await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert state is not None assert state.state == expected_state @pytest.mark.parametrize( ("entity_id", "mock_fixture"), [ ("sensor.washer_state", "mock_washer_api"), ("sensor.dryer_state", "mock_dryer_api"), ], ) @pytest.mark.parametrize( ( "filling", "rinsing", "sensing", "soaking", "spinning", "washing", "expected_state", ), [ (True, False, False, False, False, False, "cycle_filling"), (False, True, False, False, False, False, "cycle_rinsing"), (False, False, True, False, False, False, "cycle_sensing"), (False, False, False, True, False, False, "cycle_soaking"), (False, False, False, False, True, False, "cycle_spinning"), (False, False, False, False, False, True, "cycle_washing"), ], ) async def test_washer_dryer_running_states( hass: HomeAssistant, entity_id: str, mock_fixture: str, filling: bool, rinsing: bool, sensing: bool, soaking: bool, spinning: bool, washing: bool, expected_state: str, request: pytest.FixtureRequest, ) -> None: """Test Washer/Dryer machine states for RunningMainCycle.""" mock_instance = request.getfixturevalue(mock_fixture) await init_integration(hass) mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle mock_instance.get_cycle_status_filling.return_value = filling mock_instance.get_cycle_status_rinsing.return_value = rinsing mock_instance.get_cycle_status_sensing.return_value = sensing mock_instance.get_cycle_status_soaking.return_value = soaking mock_instance.get_cycle_status_spinning.return_value = spinning mock_instance.get_cycle_status_washing.return_value = washing await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert state is not None assert state.state == expected_state @pytest.mark.parametrize( ("entity_id", "mock_fixture"), [ ("sensor.washer_state", "mock_washer_api"), ("sensor.dryer_state", "mock_dryer_api"), ], ) async def test_washer_dryer_door_open_state( hass: HomeAssistant, entity_id: str, mock_fixture: str, request: pytest.FixtureRequest, ) -> None: """Test Washer/Dryer machine state when door is open.""" mock_instance = request.getfixturevalue(mock_fixture) await init_integration(hass) state = hass.states.get(entity_id) assert state.state == "running_maincycle" mock_instance.get_door_open.return_value = True await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert state.state == "door_open" mock_instance.get_door_open.return_value = False await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert state.state == "running_maincycle" @pytest.mark.parametrize( ("entity_id", "mock_fixture", "mock_method_name", "values"), [ ( "sensor.washer_detergent_level", "mock_washer_api", "get_dispense_1_level", [ (0, STATE_UNKNOWN), (1, "empty"), (2, "25"), (3, "50"), (4, "100"), (5, "active"), ], ), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_simple_enum_sensors( hass: HomeAssistant, entity_id: str, mock_fixture: str, mock_method_name: str, values: list[tuple[int, str]], request: pytest.FixtureRequest, ) -> None: """Test simple enum sensors where state maps directly from a single API value.""" await init_integration(hass) mock_instance = request.getfixturevalue(mock_fixture) mock_method = getattr(mock_instance, mock_method_name) for raw_value, expected_state in values: mock_method.return_value = raw_value await trigger_attr_callback(hass, mock_instance) state = hass.states.get(entity_id) assert state is not None assert state.state == expected_state