From d3d883358cc830e235a2ce1073fe2d4e45eb0a5a Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Sat, 13 Jun 2026 20:20:37 +0200 Subject: [PATCH] Add optimistic updates for Indevolt (#173091) --- .../components/indevolt/coordinator.py | 24 +++++++++++++++++-- homeassistant/components/indevolt/number.py | 4 +++- homeassistant/components/indevolt/select.py | 4 +++- homeassistant/components/indevolt/sensor.py | 2 +- homeassistant/components/indevolt/switch.py | 9 ++++++- tests/components/indevolt/test_number.py | 7 +++--- tests/components/indevolt/test_select.py | 9 +++---- tests/components/indevolt/test_services.py | 19 +++++++++++++++ tests/components/indevolt/test_switch.py | 14 +++++------ 9 files changed, 68 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/indevolt/coordinator.py b/homeassistant/components/indevolt/coordinator.py index 9c0bdeb70dc0..f48926a634bd 100644 --- a/homeassistant/components/indevolt/coordinator.py +++ b/homeassistant/components/indevolt/coordinator.py @@ -11,6 +11,7 @@ from indevolt_api import ( IndevoltConfig, IndevoltEnergyMode, IndevoltRealtimeAction, + IndevoltRealtimeState, ) from homeassistant.config_entries import ConfigEntry @@ -109,6 +110,10 @@ class IndevoltCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Push/write data values to given key on the device.""" return await self.api.set_data(sensor_key, value) + def async_optimistic_update(self, read_key: str, value: Any) -> None: + """Optimistically update coordinator data without fetching from device.""" + self.async_set_updated_data({**self.data, read_key: value}) + async def async_switch_energy_mode( self, target_mode: IndevoltEnergyMode, refresh: bool = True ) -> None: @@ -142,7 +147,9 @@ class IndevoltCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) if refresh: - await self.async_request_refresh() + self.async_optimistic_update( + IndevoltConfig.READ_ENERGY_MODE, target_mode + ) async def async_realtime_action( self, @@ -161,10 +168,15 @@ class IndevoltCoordinator(DataUpdateCoordinator[dict[str, Any]]): match action: case IndevoltRealtimeAction.CHARGE: success = await self.api.charge(power, target_soc) + state = IndevoltRealtimeState.CHARGING + case IndevoltRealtimeAction.DISCHARGE: success = await self.api.discharge(power, target_soc) + state = IndevoltRealtimeState.DISCHARGING + case IndevoltRealtimeAction.STOP: success = await self.api.stop() + state = IndevoltRealtimeState.STANDBY if not success: raise HomeAssistantError( @@ -172,7 +184,15 @@ class IndevoltCoordinator(DataUpdateCoordinator[dict[str, Any]]): translation_key="failed_to_execute_realtime_action", ) - await self.async_request_refresh() + self.async_set_updated_data( + { + **self.data, + IndevoltConfig.READ_ENERGY_MODE: IndevoltEnergyMode.REAL_TIME_CONTROL, + IndevoltConfig.READ_REALTIME_STATE: state, + IndevoltConfig.READ_REALTIME_TARGET_SOC: target_soc, + IndevoltConfig.READ_REALTIME_POWER_LIMIT: power, + } + ) def get_emergency_soc(self) -> int: """Get the emergency SOC value.""" diff --git a/homeassistant/components/indevolt/number.py b/homeassistant/components/indevolt/number.py index bd4ef3ba12f8..1d8d76e75ed3 100644 --- a/homeassistant/components/indevolt/number.py +++ b/homeassistant/components/indevolt/number.py @@ -136,7 +136,9 @@ class IndevoltNumberEntity(IndevoltEntity, NumberEntity): ) if success: - await self.coordinator.async_request_refresh() + self.coordinator.async_optimistic_update( + self.entity_description.read_key, int_value + ) else: raise HomeAssistantError( diff --git a/homeassistant/components/indevolt/select.py b/homeassistant/components/indevolt/select.py index 2d4b0fab634b..1008d1e64087 100644 --- a/homeassistant/components/indevolt/select.py +++ b/homeassistant/components/indevolt/select.py @@ -106,7 +106,9 @@ class IndevoltSelectEntity(IndevoltEntity, SelectEntity): ) if success: - await self.coordinator.async_request_refresh() + self.coordinator.async_optimistic_update( + self.entity_description.read_key, value + ) else: raise HomeAssistantError( diff --git a/homeassistant/components/indevolt/sensor.py b/homeassistant/components/indevolt/sensor.py index 1331d7b0356f..8738b9a26492 100644 --- a/homeassistant/components/indevolt/sensor.py +++ b/homeassistant/components/indevolt/sensor.py @@ -86,7 +86,7 @@ SENSORS: Final = ( ), # Real-time control state IndevoltSensorEntityDescription( - key=IndevoltConfig.READ_REALTIME_COMMAND, + key=IndevoltConfig.READ_REALTIME_STATE, translation_key="realtime_command", state_mapping={1000: "standby", 1001: "charging", 1002: "discharging"}, device_class=SensorDeviceClass.ENUM, diff --git a/homeassistant/components/indevolt/switch.py b/homeassistant/components/indevolt/switch.py index e08f22f3609b..d9efc05e6318 100644 --- a/homeassistant/components/indevolt/switch.py +++ b/homeassistant/components/indevolt/switch.py @@ -126,7 +126,14 @@ class IndevoltSwitchEntity(IndevoltEntity, SwitchEntity): ) if success: - await self.coordinator.async_request_refresh() + read_value = ( + self.entity_description.read_on_value + if value + else self.entity_description.read_off_value + ) + self.coordinator.async_optimistic_update( + self.entity_description.read_key, read_value + ) else: raise HomeAssistantError( diff --git a/tests/components/indevolt/test_number.py b/tests/components/indevolt/test_number.py index 1e830ca9c9c3..a3ce27f34370 100644 --- a/tests/components/indevolt/test_number.py +++ b/tests/components/indevolt/test_number.py @@ -82,10 +82,8 @@ async def test_number_set_values( # Reset mock call count for this iteration mock_indevolt.set_data.reset_mock() - # Update mock data to reflect the new value - mock_indevolt.fetch_data.return_value[read_key] = test_value - # Call the service to set the value + fetch_count_before = mock_indevolt.fetch_data.call_count await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -96,7 +94,8 @@ async def test_number_set_values( # Verify set_data was called with correct parameters mock_indevolt.set_data.assert_called_with(write_key, test_value) - # Verify updated state + # Verify state updated optimistically without a new fetch + assert mock_indevolt.fetch_data.call_count == fetch_count_before assert (state := hass.states.get(entity_id)) is not None assert int(float(state.state)) == test_value diff --git a/tests/components/indevolt/test_select.py b/tests/components/indevolt/test_select.py index 9ae3f87551fb..9f3f53954aea 100644 --- a/tests/components/indevolt/test_select.py +++ b/tests/components/indevolt/test_select.py @@ -62,12 +62,8 @@ async def test_select_option( # Reset mock call count for this iteration mock_indevolt.set_data.reset_mock() - # Update mock data to reflect the new value - mock_indevolt.fetch_data.return_value[IndevoltConfig.READ_ENERGY_MODE] = ( - expected_value - ) - # Attempt to change option + fetch_count_before = mock_indevolt.fetch_data.call_count await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, @@ -80,7 +76,8 @@ async def test_select_option( IndevoltConfig.WRITE_ENERGY_MODE, expected_value ) - # Verify updated state + # Verify state updated optimistically without a new fetch + assert mock_indevolt.fetch_data.call_count == fetch_count_before assert (state := hass.states.get("select.cms_sf2000_energy_mode")) is not None assert state.state == option diff --git a/tests/components/indevolt/test_services.py b/tests/components/indevolt/test_services.py index 637639a13adc..47a3017abdab 100644 --- a/tests/components/indevolt/test_services.py +++ b/tests/components/indevolt/test_services.py @@ -73,6 +73,25 @@ async def test_service_charge_discharge( else: mock_indevolt.discharge.assert_called_once_with(power, target_soc) + # Verify sensor states were updated optimistically + expected_rt_command = "charging" if service_name == "charge" else "discharging" + + assert (state := hass.states.get("sensor.cms_sf2000_energy_mode")) is not None + assert state.state == "real_time_control" + + assert (state := hass.states.get("sensor.cms_sf2000_real_time_mode")) is not None + assert state.state == expected_rt_command + + assert ( + state := hass.states.get("sensor.cms_sf2000_real_time_target_soc") + ) is not None + assert int(float(state.state)) == target_soc + + assert ( + state := hass.states.get("sensor.cms_sf2000_real_time_power_limit") + ) is not None + assert int(float(state.state)) == power + @pytest.mark.parametrize("generation", [1], indirect=True) @pytest.mark.parametrize( diff --git a/tests/components/indevolt/test_switch.py b/tests/components/indevolt/test_switch.py index 2e3b2753c7d8..db7233981dfa 100644 --- a/tests/components/indevolt/test_switch.py +++ b/tests/components/indevolt/test_switch.py @@ -83,10 +83,8 @@ async def test_switch_turn_on( # Reset mock call count for this iteration mock_indevolt.set_data.reset_mock() - # Update mock data to reflect the new value - mock_indevolt.fetch_data.return_value[read_key] = on_value - # Call the service to turn on + fetch_count_before = mock_indevolt.fetch_data.call_count await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -97,7 +95,8 @@ async def test_switch_turn_on( # Verify set_data was called with correct parameters mock_indevolt.set_data.assert_called_with(write_key, 1) - # Verify updated state + # Verify state updated optimistically without a new fetch + assert mock_indevolt.fetch_data.call_count == fetch_count_before assert (state := hass.states.get(entity_id)) is not None assert state.state == STATE_ON @@ -142,10 +141,8 @@ async def test_switch_turn_off( # Reset mock call count for this iteration mock_indevolt.set_data.reset_mock() - # Update mock data to reflect the new value - mock_indevolt.fetch_data.return_value[read_key] = off_value - # Call the service to turn off + fetch_count_before = mock_indevolt.fetch_data.call_count await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -156,7 +153,8 @@ async def test_switch_turn_off( # Verify set_data was called with correct parameters mock_indevolt.set_data.assert_called_with(write_key, 0) - # Verify updated state + # Verify state updated optimistically without a new fetch + assert mock_indevolt.fetch_data.call_count == fetch_count_before assert (state := hass.states.get(entity_id)) is not None assert state.state == STATE_OFF