mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 07:05:48 +01:00
Refactor optimistic update and delayed refresh for Liebherr integration (#163121)
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
"""Constants for the liebherr integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "liebherr"
|
||||
MANUFACTURER: Final = "Liebherr"
|
||||
|
||||
REFRESH_DELAY: Final = timedelta(seconds=5)
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pyliebherrhomeapi import TemperatureControl, ZonePosition
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
from typing import Any
|
||||
|
||||
from pyliebherrhomeapi import (
|
||||
LiebherrConnectionError,
|
||||
LiebherrTimeoutError,
|
||||
TemperatureControl,
|
||||
ZonePosition,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .const import DOMAIN, MANUFACTURER, REFRESH_DELAY
|
||||
from .coordinator import LiebherrCoordinator
|
||||
|
||||
# Zone position to translation key mapping
|
||||
@@ -44,6 +54,22 @@ class LiebherrEntity(CoordinatorEntity[LiebherrCoordinator]):
|
||||
model_id=device.device_name,
|
||||
)
|
||||
|
||||
async def _async_send_command(
|
||||
self,
|
||||
command: Coroutine[Any, Any, None],
|
||||
) -> None:
|
||||
"""Send a command with error handling and delayed refresh."""
|
||||
try:
|
||||
await command
|
||||
except (LiebherrConnectionError, LiebherrTimeoutError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from err
|
||||
|
||||
await asyncio.sleep(REFRESH_DELAY.total_seconds())
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
class LiebherrZoneEntity(LiebherrEntity):
|
||||
"""Base entity for zone-based Liebherr entities.
|
||||
|
||||
@@ -4,13 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyliebherrhomeapi import (
|
||||
LiebherrConnectionError,
|
||||
LiebherrTimeoutError,
|
||||
TemperatureControl,
|
||||
TemperatureUnit,
|
||||
)
|
||||
from pyliebherrhomeapi import TemperatureControl, TemperatureUnit
|
||||
|
||||
from homeassistant.components.number import (
|
||||
DEFAULT_MAX_VALUE,
|
||||
@@ -21,10 +17,8 @@ from homeassistant.components.number import (
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
|
||||
from .entity import LiebherrZoneEntity
|
||||
|
||||
@@ -109,10 +103,9 @@ class LiebherrNumber(LiebherrZoneEntity, NumberEntity):
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value."""
|
||||
# temperature_control is guaranteed to exist when entity is available
|
||||
return self.entity_description.value_fn(
|
||||
self.temperature_control # type: ignore[arg-type]
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
assert self.temperature_control is not None
|
||||
return self.entity_description.value_fn(self.temperature_control)
|
||||
|
||||
@property
|
||||
def native_min_value(self) -> float:
|
||||
@@ -139,27 +132,21 @@ class LiebherrNumber(LiebherrZoneEntity, NumberEntity):
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
# temperature_control is guaranteed to exist when entity is available
|
||||
if TYPE_CHECKING:
|
||||
assert self.temperature_control is not None
|
||||
temp_control = self.temperature_control
|
||||
|
||||
unit = (
|
||||
TemperatureUnit.FAHRENHEIT
|
||||
if temp_control.unit == TemperatureUnit.FAHRENHEIT # type: ignore[union-attr]
|
||||
if temp_control.unit == TemperatureUnit.FAHRENHEIT
|
||||
else TemperatureUnit.CELSIUS
|
||||
)
|
||||
|
||||
try:
|
||||
await self.coordinator.client.set_temperature(
|
||||
await self._async_send_command(
|
||||
self.coordinator.client.set_temperature(
|
||||
device_id=self.coordinator.device_id,
|
||||
zone_id=self._zone_id,
|
||||
target=int(value),
|
||||
unit=unit,
|
||||
)
|
||||
except (LiebherrConnectionError, LiebherrTimeoutError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
),
|
||||
)
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with the device: {error}"
|
||||
"message": "An error occurred while communicating with the device"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyliebherrhomeapi import (
|
||||
LiebherrConnectionError,
|
||||
LiebherrTimeoutError,
|
||||
ToggleControl,
|
||||
ZonePosition,
|
||||
)
|
||||
from pyliebherrhomeapi import ToggleControl, ZonePosition
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
|
||||
from .entity import ZONE_POSITION_MAP, LiebherrEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
REFRESH_DELAY = 5
|
||||
|
||||
# Control names from the API
|
||||
CONTROL_SUPERCOOL = "supercool"
|
||||
@@ -144,7 +135,6 @@ class LiebherrDeviceSwitch(LiebherrEntity, SwitchEntity):
|
||||
|
||||
entity_description: LiebherrSwitchEntityDescription
|
||||
_zone_id: int | None = None
|
||||
_optimistic_state: bool | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -171,17 +161,10 @@ class LiebherrDeviceSwitch(LiebherrEntity, SwitchEntity):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the switch is on."""
|
||||
if self._optimistic_state is not None:
|
||||
return self._optimistic_state
|
||||
if TYPE_CHECKING:
|
||||
assert self._toggle_control is not None
|
||||
return self._toggle_control.value
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._optimistic_state = None
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
@@ -205,21 +188,7 @@ class LiebherrDeviceSwitch(LiebherrEntity, SwitchEntity):
|
||||
|
||||
async def _async_set_value(self, value: bool) -> None:
|
||||
"""Set the switch value."""
|
||||
try:
|
||||
await self._async_call_set_fn(value)
|
||||
except (LiebherrConnectionError, LiebherrTimeoutError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
# Track expected state locally to avoid mutating shared coordinator data
|
||||
self._optimistic_state = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
await asyncio.sleep(REFRESH_DELAY)
|
||||
await self.coordinator.async_request_refresh()
|
||||
await self._async_send_command(self._async_call_set_fn(value))
|
||||
|
||||
|
||||
class LiebherrZoneSwitch(LiebherrDeviceSwitch):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from collections.abc import Generator
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pyliebherrhomeapi import (
|
||||
@@ -86,6 +87,16 @@ MOCK_DEVICE_STATE = DeviceState(
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_refresh_delay() -> Generator[None]:
|
||||
"""Patch REFRESH_DELAY to 0 to avoid delays in tests."""
|
||||
with patch(
|
||||
"homeassistant.components.liebherr.entity.REFRESH_DELAY",
|
||||
timedelta(seconds=0),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
|
||||
@@ -172,6 +172,8 @@ async def test_set_temperature(
|
||||
"""Test setting the temperature."""
|
||||
entity_id = "number.test_fridge_top_zone_setpoint"
|
||||
|
||||
initial_call_count = mock_liebherr_client.get_device_state.call_count
|
||||
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
@@ -186,6 +188,9 @@ async def test_set_temperature(
|
||||
unit=TemperatureUnit.CELSIUS,
|
||||
)
|
||||
|
||||
# Verify coordinator refresh was triggered
|
||||
assert mock_liebherr_client.get_device_state.call_count > initial_call_count
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_temperature_failure(
|
||||
@@ -201,7 +206,7 @@ async def test_set_temperature_failure(
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="An error occurred while communicating with the device: Connection failed",
|
||||
match="An error occurred while communicating with the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
|
||||
@@ -140,7 +140,7 @@ async def test_switch_failure(
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="An error occurred while communicating with the device: Connection failed",
|
||||
match="An error occurred while communicating with the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
|
||||
Reference in New Issue
Block a user