mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 22:37:11 +00:00
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:
parent
587ebd5d47
commit
7ada2f864c
@ -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:
|
||||
|
@ -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__(
|
||||
|
99
homeassistant/components/laundrify/sensor.py
Normal file
99
homeassistant/components/laundrify/sensor.py
Normal 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)
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -5,6 +5,7 @@
|
||||
"status": "OFF",
|
||||
"internalIP": "192.168.0.123",
|
||||
"model": "SU02",
|
||||
"firmwareVersion": "2.1.0"
|
||||
"firmwareVersion": "2.1.0",
|
||||
"totalEnergy": 1337.0
|
||||
}
|
||||
]
|
||||
|
@ -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}
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
94
tests/components/laundrify/test_sensor.py
Normal file
94
tests/components/laundrify/test_sensor.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user