Add sensor platform to laundrify integration (#121378)

* feat: initial implementation of sensor platform

* refactor(tests): await setup of config_entry in parent function

* feat(tests): add tests for laundrify sensor platform

* refactor: set name property for laundrify binary_sensor

* refactor(tests): add missing type hints

* refactor(tests): remove global change of the logging level

* refactor: address minor changes from code review

* refactor(tests): transform setup_config_entry into fixture

* refactor: leverage entity descriptions to define common entity properties

* refactor: change native unit to Wh

* fix(tests): use fixture to create the config entry

* fix: remove redundant raise of LaundrifyDeviceException

* fix(tests): raise a LaundrifyDeviceException to test the update failure behavior

* refactor(tests): merge several library fixtures into a single one

* refactor(tests): create a separate UpdateCoordinator instead of using the internal

* refactor(tests): avoid using LaundrifyPowerSensor

* refactor: simplify value retrieval by directly accessing the coordinator

* refactor: remove non-raising code from try-block

* refactor(sensor): revert usage of entity descriptions

* refactor(sensor): consolidate common attributes and init func to LaundrifyBaseSensor

* refactor(sensor): instantiate DeviceInfo obj instead of using dict

* refactor(tests): use freezer to trigger coordinator update

* refactor(tests): assert on entity state instead of coordinator

* refactor(tests): make use of freezer

* chore(tests): typo in comment
This commit is contained in:
xLarry 2024-09-16 16:21:16 +02:00 committed by GitHub
parent 587ebd5d47
commit 7ada2f864c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 331 additions and 131 deletions

View File

@ -14,7 +14,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_POLL_INTERVAL, DOMAIN
from .coordinator import LaundrifyUpdateCoordinator
PLATFORMS = [Platform.BINARY_SENSOR]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@ -44,7 +44,6 @@ class LaundrifyPowerPlug(
_attr_device_class = BinarySensorDeviceClass.RUNNING
_attr_unique_id: str
_attr_has_entity_name = True
_attr_name = None
_attr_translation_key = "wash_cycle"
def __init__(

View File

@ -0,0 +1,99 @@
"""Platform for sensor integration."""
import logging
from laundrify_aio import LaundrifyDevice
from laundrify_aio.exceptions import LaundrifyDeviceException
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import LaundrifyUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Add power sensor for passed config_entry in HA."""
coordinator: LaundrifyUpdateCoordinator = hass.data[DOMAIN][config.entry_id][
"coordinator"
]
sensor_entities: list[LaundrifyPowerSensor | LaundrifyEnergySensor] = []
for device in coordinator.data.values():
sensor_entities.append(LaundrifyPowerSensor(device))
sensor_entities.append(LaundrifyEnergySensor(coordinator, device))
async_add_entities(sensor_entities)
class LaundrifyBaseSensor(SensorEntity):
"""Base class for Laundrify sensors."""
_attr_has_entity_name = True
def __init__(self, device: LaundrifyDevice) -> None:
"""Initialize the sensor."""
self._device = device
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device.id)})
self._attr_unique_id = f"{device.id}_{self._attr_device_class}"
class LaundrifyPowerSensor(LaundrifyBaseSensor):
"""Representation of a Power sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_suggested_display_precision = 0
async def async_update(self) -> None:
"""Fetch latest power measurement from the device."""
try:
power = await self._device.get_power()
except LaundrifyDeviceException as err:
_LOGGER.debug("Couldn't load power for %s: %s", self._attr_unique_id, err)
self._attr_available = False
else:
_LOGGER.debug("Retrieved power for %s: %s", self._attr_unique_id, power)
if power is not None:
self._attr_available = True
self._attr_native_value = power
class LaundrifyEnergySensor(
CoordinatorEntity[LaundrifyUpdateCoordinator], LaundrifyBaseSensor
):
"""Representation of an Energy sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_suggested_display_precision = 2
def __init__(
self, coordinator: LaundrifyUpdateCoordinator, device: LaundrifyDevice
) -> None:
"""Initialize the sensor."""
CoordinatorEntity.__init__(self, coordinator)
LaundrifyBaseSensor.__init__(self, device)
@property
def native_value(self) -> float:
"""Return the total energy of the device."""
device = self.coordinator.data[self._device.id]
return float(device.totalEnergy)

View File

@ -1,22 +1 @@
"""Tests for the laundrify integration."""
from homeassistant.components.laundrify import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from .const import VALID_ACCESS_TOKEN, VALID_ACCOUNT_ID
from tests.common import MockConfigEntry
def create_entry(
hass: HomeAssistant, access_token: str = VALID_ACCESS_TOKEN
) -> MockConfigEntry:
"""Create laundrify entry in Home Assistant."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=VALID_ACCOUNT_ID,
data={CONF_ACCESS_TOKEN: access_token},
)
entry.add_to_hass(hass)
return entry

View File

@ -1,59 +1,75 @@
"""Configure py.test."""
import json
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from laundrify_aio import LaundrifyAPI, LaundrifyDevice
import pytest
from homeassistant.components.laundrify import DOMAIN
from homeassistant.components.laundrify.const import MANUFACTURER
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from .const import VALID_ACCESS_TOKEN, VALID_ACCOUNT_ID
from tests.common import load_fixture
from tests.common import MockConfigEntry, load_fixture
from tests.typing import ClientSessionGenerator
@pytest.fixture(name="laundrify_setup_entry")
def laundrify_setup_entry_fixture():
"""Mock laundrify setup entry function."""
with patch(
"homeassistant.components.laundrify.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(name="mock_device")
def laundrify_sensor_fixture() -> LaundrifyDevice:
"""Return a default Laundrify power sensor mock."""
# Load test data from machines.json
machine_data = json.loads(load_fixture("laundrify/machines.json"))[0]
mock_device = AsyncMock(spec=LaundrifyDevice)
mock_device.id = machine_data["id"]
mock_device.manufacturer = MANUFACTURER
mock_device.model = machine_data["model"]
mock_device.name = machine_data["name"]
mock_device.firmwareVersion = machine_data["firmwareVersion"]
return mock_device
@pytest.fixture(name="laundrify_exchange_code")
def laundrify_exchange_code_fixture():
"""Mock laundrify exchange_auth_code function."""
with patch(
"laundrify_aio.LaundrifyAPI.exchange_auth_code",
return_value=VALID_ACCESS_TOKEN,
) as exchange_code_mock:
yield exchange_code_mock
@pytest.fixture(name="laundrify_validate_token")
def laundrify_validate_token_fixture():
"""Mock laundrify validate_token function."""
with patch(
"laundrify_aio.LaundrifyAPI.validate_token",
return_value=True,
) as validate_token_mock:
yield validate_token_mock
@pytest.fixture(name="laundrify_config_entry")
async def laundrify_setup_config_entry(
hass: HomeAssistant, access_token: str = VALID_ACCESS_TOKEN
) -> MockConfigEntry:
"""Create laundrify entry in Home Assistant."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=VALID_ACCOUNT_ID,
data={CONF_ACCESS_TOKEN: access_token},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
@pytest.fixture(name="laundrify_api_mock", autouse=True)
def laundrify_api_fixture(laundrify_exchange_code, laundrify_validate_token):
def laundrify_api_fixture(hass_client: ClientSessionGenerator):
"""Mock valid laundrify API responses."""
with (
patch(
"laundrify_aio.LaundrifyAPI.get_account_id",
return_value=VALID_ACCOUNT_ID,
),
patch(
"laundrify_aio.LaundrifyAPI.validate_token",
return_value=True,
),
patch(
"laundrify_aio.LaundrifyAPI.exchange_auth_code",
return_value=VALID_ACCESS_TOKEN,
),
patch(
"laundrify_aio.LaundrifyAPI.get_machines",
return_value=[
LaundrifyDevice(machine, LaundrifyAPI)
for machine in json.loads(load_fixture("laundrify/machines.json"))
],
) as get_machines_mock,
),
):
yield get_machines_mock
yield LaundrifyAPI(VALID_ACCESS_TOKEN, hass_client)

View File

@ -5,6 +5,7 @@
"status": "OFF",
"internalIP": "192.168.0.123",
"model": "SU02",
"firmwareVersion": "2.1.0"
"firmwareVersion": "2.1.0",
"totalEnergy": 1337.0
}
]

View File

@ -8,11 +8,12 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CODE, CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from . import create_entry
from .const import VALID_ACCESS_TOKEN, VALID_AUTH_CODE, VALID_USER_INPUT
from tests.common import MockConfigEntry
async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None:
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@ -31,14 +32,11 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None:
assert result["data"] == {
CONF_ACCESS_TOKEN: VALID_ACCESS_TOKEN,
}
assert len(laundrify_setup_entry.mock_calls) == 1
async def test_form_invalid_format(
hass: HomeAssistant, laundrify_exchange_code
) -> None:
async def test_form_invalid_format(hass: HomeAssistant, laundrify_api_mock) -> None:
"""Test we handle invalid format."""
laundrify_exchange_code.side_effect = exceptions.InvalidFormat
laundrify_api_mock.exchange_auth_code.side_effect = exceptions.InvalidFormat
result = await hass.config_entries.flow.async_init(
DOMAIN,
@ -50,9 +48,9 @@ async def test_form_invalid_format(
assert result["errors"] == {CONF_CODE: "invalid_format"}
async def test_form_invalid_auth(hass: HomeAssistant, laundrify_exchange_code) -> None:
async def test_form_invalid_auth(hass: HomeAssistant, laundrify_api_mock) -> None:
"""Test we handle invalid auth."""
laundrify_exchange_code.side_effect = exceptions.UnknownAuthCode
laundrify_api_mock.exchange_auth_code.side_effect = exceptions.UnknownAuthCode
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
@ -63,11 +61,11 @@ async def test_form_invalid_auth(hass: HomeAssistant, laundrify_exchange_code) -
assert result["errors"] == {CONF_CODE: "invalid_auth"}
async def test_form_cannot_connect(
hass: HomeAssistant, laundrify_exchange_code
) -> None:
async def test_form_cannot_connect(hass: HomeAssistant, laundrify_api_mock) -> None:
"""Test we handle cannot connect error."""
laundrify_exchange_code.side_effect = exceptions.ApiConnectionException
laundrify_api_mock.exchange_auth_code.side_effect = (
exceptions.ApiConnectionException
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
@ -78,11 +76,9 @@ async def test_form_cannot_connect(
assert result["errors"] == {"base": "cannot_connect"}
async def test_form_unkown_exception(
hass: HomeAssistant, laundrify_exchange_code
) -> None:
async def test_form_unkown_exception(hass: HomeAssistant, laundrify_api_mock) -> None:
"""Test we handle all other errors."""
laundrify_exchange_code.side_effect = Exception
laundrify_api_mock.exchange_auth_code.side_effect = Exception
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
@ -93,10 +89,11 @@ async def test_form_unkown_exception(
assert result["errors"] == {"base": "unknown"}
async def test_step_reauth(hass: HomeAssistant) -> None:
async def test_step_reauth(
hass: HomeAssistant, laundrify_config_entry: MockConfigEntry
) -> None:
"""Test the reauth form is shown."""
config_entry = create_entry(hass)
result = await config_entry.start_reauth_flow(hass)
result = await laundrify_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["errors"] is None
@ -110,9 +107,10 @@ async def test_step_reauth(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM
async def test_integration_already_exists(hass: HomeAssistant) -> None:
async def test_integration_already_exists(
hass: HomeAssistant, laundrify_config_entry: MockConfigEntry
) -> None:
"""Test we only allow a single config flow."""
create_entry(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)

View File

@ -1,52 +1,70 @@
"""Test the laundrify coordinator."""
from laundrify_aio import exceptions
from datetime import timedelta
from homeassistant.components.laundrify.const import DOMAIN
from homeassistant.core import HomeAssistant
from freezegun.api import FrozenDateTimeFactory
from laundrify_aio import LaundrifyDevice, exceptions
from . import create_entry
from homeassistant.components.laundrify.const import DEFAULT_POLL_INTERVAL
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant, State
from homeassistant.util import slugify
from tests.common import async_fire_time_changed
async def test_coordinator_update_success(hass: HomeAssistant) -> None:
def get_coord_entity(hass: HomeAssistant, mock_device: LaundrifyDevice) -> State:
"""Get the coordinated energy sensor entity."""
device_slug = slugify(mock_device.name, separator="_")
return hass.states.get(f"sensor.{device_slug}_energy")
async def test_coordinator_update_success(
hass: HomeAssistant,
laundrify_config_entry,
mock_device: LaundrifyDevice,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the coordinator update is performed successfully."""
config_entry = create_entry(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
await coordinator.async_refresh()
freezer.tick(timedelta(seconds=DEFAULT_POLL_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert coordinator.last_update_success
coord_entity = get_coord_entity(hass, mock_device)
assert coord_entity.state != STATE_UNAVAILABLE
async def test_coordinator_update_unauthorized(
hass: HomeAssistant, laundrify_api_mock
hass: HomeAssistant,
laundrify_config_entry,
laundrify_api_mock,
mock_device: LaundrifyDevice,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the coordinator update fails if an UnauthorizedException is thrown."""
config_entry = create_entry(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
laundrify_api_mock.get_machines.side_effect = exceptions.UnauthorizedException
freezer.tick(timedelta(seconds=DEFAULT_POLL_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
laundrify_api_mock.side_effect = exceptions.UnauthorizedException
await coordinator.async_refresh()
await hass.async_block_till_done()
assert not coordinator.last_update_success
coord_entity = get_coord_entity(hass, mock_device)
assert coord_entity.state == STATE_UNAVAILABLE
async def test_coordinator_update_connection_failed(
hass: HomeAssistant, laundrify_api_mock
hass: HomeAssistant,
laundrify_config_entry,
laundrify_api_mock,
mock_device: LaundrifyDevice,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the coordinator update fails if an ApiConnectionException is thrown."""
config_entry = create_entry(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
laundrify_api_mock.get_machines.side_effect = exceptions.ApiConnectionException
freezer.tick(timedelta(seconds=DEFAULT_POLL_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
laundrify_api_mock.side_effect = exceptions.ApiConnectionException
await coordinator.async_refresh()
await hass.async_block_till_done()
assert not coordinator.last_update_success
coord_entity = get_coord_entity(hass, mock_device)
assert coord_entity.state == STATE_UNAVAILABLE

View File

@ -6,54 +6,50 @@ from homeassistant.components.laundrify.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import create_entry
from tests.common import MockConfigEntry
async def test_setup_entry_api_unauthorized(
hass: HomeAssistant, laundrify_validate_token
hass: HomeAssistant,
laundrify_api_mock,
laundrify_config_entry: MockConfigEntry,
) -> None:
"""Test that ConfigEntryAuthFailed is thrown when authentication fails."""
laundrify_validate_token.side_effect = exceptions.UnauthorizedException
config_entry = create_entry(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
laundrify_api_mock.validate_token.side_effect = exceptions.UnauthorizedException
await hass.config_entries.async_reload(laundrify_config_entry.entry_id)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.SETUP_ERROR
assert laundrify_config_entry.state is ConfigEntryState.SETUP_ERROR
assert not hass.data.get(DOMAIN)
async def test_setup_entry_api_cannot_connect(
hass: HomeAssistant, laundrify_validate_token
hass: HomeAssistant,
laundrify_api_mock,
laundrify_config_entry: MockConfigEntry,
) -> None:
"""Test that ApiConnectionException is thrown when connection fails."""
laundrify_validate_token.side_effect = exceptions.ApiConnectionException
config_entry = create_entry(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
laundrify_api_mock.validate_token.side_effect = exceptions.ApiConnectionException
await hass.config_entries.async_reload(laundrify_config_entry.entry_id)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert laundrify_config_entry.state is ConfigEntryState.SETUP_RETRY
assert not hass.data.get(DOMAIN)
async def test_setup_entry_successful(hass: HomeAssistant) -> None:
async def test_setup_entry_successful(
hass: HomeAssistant, laundrify_config_entry: MockConfigEntry
) -> None:
"""Test entry can be setup successfully."""
config_entry = create_entry(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.LOADED
assert laundrify_config_entry.state is ConfigEntryState.LOADED
async def test_setup_entry_unload(hass: HomeAssistant) -> None:
async def test_setup_entry_unload(
hass: HomeAssistant, laundrify_config_entry: MockConfigEntry
) -> None:
"""Test unloading the laundrify entry."""
config_entry = create_entry(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.config_entries.async_unload(laundrify_config_entry.entry_id)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert laundrify_config_entry.state is ConfigEntryState.NOT_LOADED

View File

@ -0,0 +1,94 @@
"""Test the laundrify sensor platform."""
from datetime import timedelta
import logging
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
from laundrify_aio import LaundrifyDevice
from laundrify_aio.exceptions import LaundrifyDeviceException
import pytest
from homeassistant.components.laundrify.const import (
DEFAULT_POLL_INTERVAL,
DOMAIN,
MODELS,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
STATE_UNKNOWN,
UnitOfPower,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.util import slugify
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_laundrify_sensor_init(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_device: LaundrifyDevice,
laundrify_config_entry: MockConfigEntry,
) -> None:
"""Test Laundrify sensor default state."""
device_slug = slugify(mock_device.name, separator="_")
state = hass.states.get(f"sensor.{device_slug}_power")
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER
assert state.state == STATE_UNKNOWN
device = device_registry.async_get_device({(DOMAIN, mock_device.id)})
assert device is not None
assert device.name == mock_device.name
assert device.identifiers == {(DOMAIN, mock_device.id)}
assert device.manufacturer == mock_device.manufacturer
assert device.model == MODELS[mock_device.model]
assert device.sw_version == mock_device.firmwareVersion
async def test_laundrify_sensor_update(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_device: LaundrifyDevice,
laundrify_config_entry: MockConfigEntry,
) -> None:
"""Test Laundrify sensor update."""
device_slug = slugify(mock_device.name, separator="_")
state = hass.states.get(f"sensor.{device_slug}_power")
assert state.state == STATE_UNKNOWN
with patch("laundrify_aio.LaundrifyDevice.get_power", return_value=95):
freezer.tick(timedelta(seconds=DEFAULT_POLL_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(f"sensor.{device_slug}_power")
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT
assert state.state == "95"
async def test_laundrify_sensor_update_failure(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
mock_device: LaundrifyDevice,
laundrify_config_entry: MockConfigEntry,
) -> None:
"""Test that update failures are logged."""
caplog.set_level(logging.DEBUG)
# test get_power() to raise a LaundrifyDeviceException
with patch(
"laundrify_aio.LaundrifyDevice.get_power",
side_effect=LaundrifyDeviceException("Raising error to test update failure."),
):
freezer.tick(timedelta(seconds=DEFAULT_POLL_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert f"Couldn't load power for {mock_device.id}_power" in caplog.text