Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions homeassistant/components/indevolt/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from dataclasses import dataclass, field
from typing import Final

from indevolt_api import IndevoltRealtimeAction

from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
Expand Down Expand Up @@ -66,5 +68,4 @@ def __init__(

async def async_press(self) -> None:
"""Handle the button press."""

await self.coordinator.async_execute_realtime_action([0, 0, 0])
await self.coordinator.async_realtime_action(IndevoltRealtimeAction.STOP)
3 changes: 1 addition & 2 deletions homeassistant/components/indevolt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
ENERGY_MODE_WRITE_KEY: Final = "47005"
PORTABLE_MODE: Final = 0

# API write key and value for real-time control mode
REALTIME_ACTION_KEY: Final = "47015"
# Value for real-time control mode
REALTIME_ACTION_MODE: Final = 4

# API key fields
Expand Down
20 changes: 8 additions & 12 deletions homeassistant/components/indevolt/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any, Final

from aiohttp import ClientError
from indevolt_api import IndevoltAPI, TimeOutException
from indevolt_api import IndevoltAPI, IndevoltRealtimeAction, TimeOutException

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MODEL
Expand All @@ -24,7 +24,6 @@
ENERGY_MODE_READ_KEY,
ENERGY_MODE_WRITE_KEY,
PORTABLE_MODE,
REALTIME_ACTION_KEY,
REALTIME_ACTION_MODE,
SENSOR_KEYS,
)
Expand Down Expand Up @@ -146,19 +145,16 @@ async def async_switch_energy_mode(
if refresh:
await self.async_request_refresh()

async def async_execute_realtime_action(self, action: list[int]) -> None:
async def async_realtime_action(
self,
action_code: IndevoltRealtimeAction,
) -> None:
"""Switch mode, execute action, and refresh for real-time control."""

await self.async_switch_energy_mode(REALTIME_ACTION_MODE, refresh=False)

try:
success = await self.async_push_data(REALTIME_ACTION_KEY, action)

except (DeviceTimeoutError, DeviceConnectionError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="failed_to_execute_realtime_action",
) from err
match action_code:
case IndevoltRealtimeAction.STOP:
success = await self.api.stop()
Comment thread
Xirt marked this conversation as resolved.

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure async_realtime_action always defines success by handling all IndevoltRealtimeAction values (e.g., CHARGE/DISCHARGE) or adding a default case that raises a clear error; otherwise non-STOP actions will crash with an unbound local error.

Suggested change
success = await self.api.stop()
success = await self.api.stop()
case _:
raise HomeAssistantError(
f"Unsupported real-time action: {action_code}"
)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is currently only one caller of this function which only uses IndevoltRealtimeAction.STOP - there are thus no uncovered edge cases. Other cases will be added in subsequent PRs in which we can be exhaustive.

Comment thread
Xirt marked this conversation as resolved.

if not success:
raise HomeAssistantError(
Expand Down
3 changes: 3 additions & 0 deletions tests/components/indevolt/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ def mock_indevolt(generation: int) -> Generator[AsyncMock]:
client = mock_client.return_value
client.fetch_data.return_value = fixture_data
client.set_data.return_value = True
client.stop.return_value = True
client.charge.return_value = True
client.discharge.return_value = True
client.get_config.return_value = {
"device": {
"sn": device_info["sn"],
Expand Down
28 changes: 12 additions & 16 deletions tests/components/indevolt/test_button.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""Tests for the Indevolt button platform."""

from unittest.mock import AsyncMock, call, patch
from unittest.mock import AsyncMock, patch

from indevolt_api import TimeOutException
import pytest
from syrupy.assertion import SnapshotAssertion

Expand All @@ -11,7 +10,6 @@
ENERGY_MODE_READ_KEY,
ENERGY_MODE_WRITE_KEY,
PORTABLE_MODE,
REALTIME_ACTION_KEY,
REALTIME_ACTION_MODE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
Expand Down Expand Up @@ -64,14 +62,11 @@ async def test_button_press_standby(
blocking=True,
)

# Verify set_data was called twice with correct parameters
assert mock_indevolt.set_data.call_count == 2
mock_indevolt.set_data.assert_has_calls(
[
call(ENERGY_MODE_WRITE_KEY, REALTIME_ACTION_MODE),
call(REALTIME_ACTION_KEY, [0, 0, 0]),
]
# Verify set_data was called for mode switch and stop() was called
mock_indevolt.set_data.assert_called_once_with(
ENERGY_MODE_WRITE_KEY, REALTIME_ACTION_MODE
)
mock_indevolt.stop.assert_called_once()


@pytest.mark.parametrize("generation", [2], indirect=True)
Expand All @@ -98,22 +93,23 @@ async def test_button_press_standby_already_in_realtime_mode(
blocking=True,
)

# Verify set_data was called once with correct parameters
mock_indevolt.set_data.assert_called_once_with(REALTIME_ACTION_KEY, [0, 0, 0])
# Verify stop() was called and no mode switch was needed
mock_indevolt.set_data.assert_not_called()
mock_indevolt.stop.assert_called_once()


@pytest.mark.parametrize("generation", [2], indirect=True)
async def test_button_press_standby_timeout_error(
async def test_button_press_standby_rejected_command(
hass: HomeAssistant,
mock_indevolt: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test pressing standby raises HomeAssistantError when the device times out."""
"""Test pressing standby raises HomeAssistantError when the device rejects the command."""
with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BUTTON]):
await setup_integration(hass, mock_config_entry)

# Simulate an API push failure
mock_indevolt.set_data.side_effect = TimeOutException("Timed out")
# Simulate stop() returning False (device rejected the command)
mock_indevolt.stop.return_value = False

# Mock call to pause (dis)charging
with pytest.raises(HomeAssistantError):
Expand Down
Loading