514 lines
16 KiB
Python
514 lines
16 KiB
Python
"""Test ESPHome sensors."""
|
|
|
|
import logging
|
|
import math
|
|
|
|
from aioesphomeapi import (
|
|
APIClient,
|
|
EntityCategory as ESPHomeEntityCategory,
|
|
LastResetType,
|
|
SensorInfo,
|
|
SensorState,
|
|
SensorStateClass as ESPHomeSensorStateClass,
|
|
TextSensorInfo,
|
|
TextSensorState,
|
|
)
|
|
import pytest
|
|
|
|
from homeassistant.components.sensor import (
|
|
ATTR_STATE_CLASS,
|
|
SensorDeviceClass,
|
|
SensorStateClass,
|
|
async_rounded_state,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_DEVICE_CLASS,
|
|
ATTR_ICON,
|
|
ATTR_UNIT_OF_MEASUREMENT,
|
|
PERCENTAGE,
|
|
STATE_UNKNOWN,
|
|
EntityCategory,
|
|
UnitOfElectricCurrent,
|
|
UnitOfElectricPotential,
|
|
UnitOfEnergy,
|
|
UnitOfPower,
|
|
UnitOfPressure,
|
|
UnitOfTemperature,
|
|
UnitOfVolume,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
from .conftest import MockESPHomeDeviceType, MockGenericDeviceEntryType
|
|
|
|
|
|
async def test_generic_numeric_sensor(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_esphome_device: MockESPHomeDeviceType,
|
|
) -> None:
|
|
"""Test a generic sensor entity."""
|
|
logging.getLogger("homeassistant.components.esphome").setLevel(logging.DEBUG)
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=50)]
|
|
user_service = []
|
|
mock_device = await mock_esphome_device(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "50"
|
|
|
|
# Test updating state
|
|
mock_device.set_state(SensorState(key=1, state=60))
|
|
await hass.async_block_till_done()
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "60"
|
|
|
|
# Test sending the same state again
|
|
mock_device.set_state(SensorState(key=1, state=60))
|
|
await hass.async_block_till_done()
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "60"
|
|
|
|
# Test we can still update after the same state
|
|
mock_device.set_state(SensorState(key=1, state=70))
|
|
await hass.async_block_till_done()
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "70"
|
|
|
|
# Test invalid data from the underlying api does not crash us
|
|
mock_device.set_state(SensorState(key=1, state=object()))
|
|
await hass.async_block_till_done()
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "70"
|
|
|
|
|
|
async def test_generic_numeric_sensor_with_entity_category_and_icon(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a generic sensor entity."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
|
|
icon="mdi:leaf",
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=50)]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "50"
|
|
assert state.attributes[ATTR_ICON] == "mdi:leaf"
|
|
entry = entity_registry.async_get("sensor.test_my_sensor")
|
|
assert entry is not None
|
|
# Note that ESPHome includes the EntityInfo type in the unique id
|
|
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
|
|
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
|
|
assert entry.entity_category is EntityCategory.DIAGNOSTIC
|
|
|
|
|
|
async def test_generic_numeric_sensor_state_class_measurement(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a generic sensor entity."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
state_class=ESPHomeSensorStateClass.MEASUREMENT,
|
|
device_class="power",
|
|
unit_of_measurement="W",
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=50)]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "50"
|
|
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
|
|
entry = entity_registry.async_get("sensor.test_my_sensor")
|
|
assert entry is not None
|
|
# Note that ESPHome includes the EntityInfo type in the unique id
|
|
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
|
|
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
|
|
assert entry.entity_category is None
|
|
|
|
|
|
async def test_generic_numeric_sensor_device_class_timestamp(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a sensor entity that uses timestamp (epoch)."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
device_class="timestamp",
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=1687459432.466624)]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "2023-06-22T18:43:52+00:00"
|
|
|
|
|
|
async def test_generic_numeric_sensor_legacy_last_reset_convert(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a state class of measurement with last reset type of auto is converted to total increasing."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
legacy_last_reset_type=LastResetType.AUTO,
|
|
state_class=ESPHomeSensorStateClass.MEASUREMENT,
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=50)]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "50"
|
|
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING
|
|
|
|
|
|
async def test_generic_numeric_sensor_no_state(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a generic numeric sensor that has no state."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
)
|
|
]
|
|
states = []
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
|
|
async def test_generic_numeric_sensor_nan_state(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a generic numeric sensor that has nan state."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=math.nan, missing_state=False)]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
|
|
async def test_generic_numeric_sensor_missing_state(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a generic numeric sensor that is missing state."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=True, missing_state=True)]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
|
|
async def test_generic_text_sensor(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a generic text sensor entity."""
|
|
entity_info = [
|
|
TextSensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
)
|
|
]
|
|
states = [TextSensorState(key=1, state="i am a teapot")]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "i am a teapot"
|
|
|
|
|
|
async def test_generic_text_sensor_missing_state(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a generic text sensor that is missing state."""
|
|
entity_info = [
|
|
TextSensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
)
|
|
]
|
|
states = [TextSensorState(key=1, state=True, missing_state=True)]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
|
|
async def test_generic_text_sensor_device_class_timestamp(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a sensor entity that uses timestamp (datetime)."""
|
|
entity_info = [
|
|
TextSensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
device_class=SensorDeviceClass.TIMESTAMP,
|
|
)
|
|
]
|
|
states = [TextSensorState(key=1, state="2023-06-22T18:43:52+00:00")]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "2023-06-22T18:43:52+00:00"
|
|
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP
|
|
|
|
|
|
async def test_generic_text_sensor_device_class_date(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a sensor entity that uses date (datetime)."""
|
|
entity_info = [
|
|
TextSensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
device_class=SensorDeviceClass.DATE,
|
|
)
|
|
]
|
|
states = [TextSensorState(key=1, state="2023-06-22T18:43:52+00:00")]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "2023-06-22"
|
|
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DATE
|
|
|
|
|
|
async def test_generic_numeric_sensor_empty_string_uom(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_generic_device_entry: MockGenericDeviceEntryType,
|
|
) -> None:
|
|
"""Test a generic numeric sensor that has an empty string as the uom."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
unit_of_measurement="",
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=123, missing_state=False)]
|
|
user_service = []
|
|
await mock_generic_device_entry(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
user_service=user_service,
|
|
states=states,
|
|
)
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert state.state == "123"
|
|
assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("device_class", "unit_of_measurement", "state_value", "expected_precision"),
|
|
[
|
|
(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, 23.456, 1),
|
|
(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, 0.1, 1),
|
|
(SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, -25.789, 1),
|
|
(SensorDeviceClass.POWER, UnitOfPower.WATT, 1234.56, 0),
|
|
(SensorDeviceClass.POWER, UnitOfPower.WATT, 1.23456, 3),
|
|
(SensorDeviceClass.POWER, UnitOfPower.WATT, 0.123, 3),
|
|
(SensorDeviceClass.ENERGY, UnitOfEnergy.WATT_HOUR, 1234.5, 0),
|
|
(SensorDeviceClass.ENERGY, UnitOfEnergy.WATT_HOUR, 12.3456, 2),
|
|
(SensorDeviceClass.VOLTAGE, UnitOfElectricPotential.VOLT, 230.45, 1),
|
|
(SensorDeviceClass.VOLTAGE, UnitOfElectricPotential.VOLT, 3.3, 1),
|
|
(SensorDeviceClass.CURRENT, UnitOfElectricCurrent.AMPERE, 15.678, 2),
|
|
(SensorDeviceClass.CURRENT, UnitOfElectricCurrent.AMPERE, 0.015, 3),
|
|
(SensorDeviceClass.ATMOSPHERIC_PRESSURE, UnitOfPressure.HPA, 1013.25, 1),
|
|
(SensorDeviceClass.PRESSURE, UnitOfPressure.BAR, 1.01325, 3),
|
|
(SensorDeviceClass.VOLUME, UnitOfVolume.LITERS, 45.67, 1),
|
|
(SensorDeviceClass.VOLUME, UnitOfVolume.LITERS, 4567.0, 0),
|
|
(SensorDeviceClass.HUMIDITY, PERCENTAGE, 87.654, 1),
|
|
(SensorDeviceClass.HUMIDITY, PERCENTAGE, 45.2, 1),
|
|
(SensorDeviceClass.BATTERY, PERCENTAGE, 95.2, 1),
|
|
(SensorDeviceClass.BATTERY, PERCENTAGE, 100.0, 1),
|
|
],
|
|
)
|
|
async def test_suggested_display_precision_by_device_class(
|
|
hass: HomeAssistant,
|
|
mock_client: APIClient,
|
|
mock_esphome_device: MockESPHomeDeviceType,
|
|
device_class: SensorDeviceClass,
|
|
unit_of_measurement: str,
|
|
state_value: float,
|
|
expected_precision: int,
|
|
) -> None:
|
|
"""Test suggested display precision for different device classes."""
|
|
entity_info = [
|
|
SensorInfo(
|
|
object_id="mysensor",
|
|
key=1,
|
|
name="my sensor",
|
|
unique_id="my_sensor",
|
|
accuracy_decimals=expected_precision,
|
|
device_class=device_class.value,
|
|
unit_of_measurement=unit_of_measurement,
|
|
)
|
|
]
|
|
states = [SensorState(key=1, state=state_value)]
|
|
await mock_esphome_device(
|
|
mock_client=mock_client,
|
|
entity_info=entity_info,
|
|
states=states,
|
|
)
|
|
|
|
state = hass.states.get("sensor.test_my_sensor")
|
|
assert state is not None
|
|
assert float(
|
|
async_rounded_state(hass, "sensor.test_my_sensor", state)
|
|
) == pytest.approx(round(state_value, expected_precision))
|