-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
10 changed files
with
328 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 .const import VALID_ACCESS_TOKEN, VALID_ACCOUNT_ID | ||
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 tests.common import load_fixture | ||
from .const import VALID_ACCESS_TOKEN, VALID_ACCOUNT_ID | ||
|
||
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] | ||
|
||
@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 | ||
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_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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.