diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index d0e6c935822..6ac47d2032f 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -9,6 +9,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest from pytradfri.device import Device from pytradfri.device.air_purifier import AirPurifier +from pytradfri.device.blind import Blind from homeassistant.components.tradfri.const import DOMAIN @@ -102,3 +103,18 @@ def air_purifier(air_purifier_response: dict[str, Any]) -> AirPurifier: air_purifier_control = device.air_purifier_control assert air_purifier_control return air_purifier_control.air_purifiers[0] + + +@pytest.fixture(scope="session") +def blind_response() -> dict[str, Any]: + """Return a blind response.""" + return json.loads(load_fixture("blind.json", DOMAIN)) + + +@pytest.fixture +def blind(blind_response: dict[str, Any]) -> Blind: + """Return blind.""" + device = Device(blind_response) + blind_control = device.blind_control + assert blind_control + return blind_control.blinds[0] diff --git a/tests/components/tradfri/fixtures/remote_control.json b/tests/components/tradfri/fixtures/remote_control.json new file mode 100644 index 00000000000..722b5863089 --- /dev/null +++ b/tests/components/tradfri/fixtures/remote_control.json @@ -0,0 +1,17 @@ +{ + "3": { + "0": "IKEA of Sweden", + "1": "TRADFRI remote control", + "2": "", + "3": "1.2.214", + "6": 3, + "9": 87 + }, + "5750": 0, + "9001": "Test", + "9002": 1509923521, + "9003": 65536, + "9019": 1, + "9020": 1510010209, + "9054": 0 +} diff --git a/tests/components/tradfri/test_cover.py b/tests/components/tradfri/test_cover.py index d4b110948ab..73d2d91a638 100644 --- a/tests/components/tradfri/test_cover.py +++ b/tests/components/tradfri/test_cover.py @@ -1,39 +1,19 @@ """Tradfri cover (recognised as blinds in the IKEA ecosystem) platform tests.""" from __future__ import annotations -import json from typing import Any from unittest.mock import MagicMock, Mock import pytest from pytradfri.const import ATTR_REACHABLE_STATE -from pytradfri.device import Device from pytradfri.device.blind import Blind from homeassistant.components.cover import ATTR_CURRENT_POSITION, DOMAIN as COVER_DOMAIN -from homeassistant.components.tradfri.const import DOMAIN from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from .common import setup_integration, trigger_observe_callback -from tests.common import load_fixture - - -@pytest.fixture(scope="module") -def blind_response() -> dict[str, Any]: - """Return a blind response.""" - return json.loads(load_fixture("blind.json", DOMAIN)) - - -@pytest.fixture -def blind(blind_response: dict[str, Any]) -> Blind: - """Return blind.""" - device = Device(blind_response) - blind_control = device.blind_control - assert blind_control - return blind_control.blinds[0] - async def test_cover_available( hass: HomeAssistant, diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py index 25b6acea0de..23391c8e875 100644 --- a/tests/components/tradfri/test_sensor.py +++ b/tests/components/tradfri/test_sensor.py @@ -1,203 +1,201 @@ """Tradfri sensor platform tests.""" from __future__ import annotations -from unittest.mock import MagicMock, Mock, PropertyMock, patch +import json +from typing import Any +from unittest.mock import MagicMock, Mock import pytest -from pytradfri.device.air_purifier_control import AirPurifierControl +from pytradfri.const import ( + ATTR_AIR_PURIFIER_AIR_QUALITY, + ATTR_DEVICE_BATTERY, + ATTR_DEVICE_INFO, + ATTR_REACHABLE_STATE, + ROOT_AIR_PURIFIER, +) +from pytradfri.device import Device +from pytradfri.device.air_purifier import AirPurifier +from pytradfri.device.blind import Blind -from homeassistant.components import tradfri +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.components.tradfri.const import DOMAIN +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + PERCENTAGE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfTime, +) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import GATEWAY_ID -from .common import setup_integration +from .common import setup_integration, trigger_observe_callback -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture -@pytest.fixture(autouse=True) -def setup(request): - """Set up patches for pytradfri methods for the fan platform. - - This is used in test_fan as well as in test_sensor. - """ - with patch( - "pytradfri.device.AirPurifierControl.raw", - new_callable=PropertyMock, - return_value=[{"mock": "mock"}], - ), patch( - "pytradfri.device.AirPurifierControl.air_purifiers", - ): - yield +@pytest.fixture(scope="module") +def remote_control_response() -> dict[str, Any]: + """Return a remote control response.""" + return json.loads(load_fixture("remote_control.json", DOMAIN)) -def mock_fan(test_features=None, test_state=None, device_number=0): - """Mock a tradfri fan/air purifier.""" - if test_features is None: - test_features = {} - if test_state is None: - test_state = {} - mock_fan_data = Mock(**test_state) - - dev_info_mock = MagicMock() - dev_info_mock.manufacturer = "manufacturer" - dev_info_mock.model_number = "model" - dev_info_mock.firmware_version = "1.2.3" - _mock_fan = Mock( - id=f"mock-fan-id-{device_number}", - reachable=True, - observe=Mock(), - device_info=dev_info_mock, - has_light_control=False, - has_socket_control=False, - has_blind_control=False, - has_signal_repeater_control=False, - has_air_purifier_control=True, - ) - _mock_fan.name = f"tradfri_fan_{device_number}" - air_purifier_control = AirPurifierControl(_mock_fan) - - # Store the initial state. - setattr(air_purifier_control, "air_purifiers", [mock_fan_data]) - _mock_fan.air_purifier_control = air_purifier_control - return _mock_fan +@pytest.fixture +def remote_control(remote_control_response: dict[str, Any]) -> Device: + """Return remote control.""" + return Device(remote_control_response) -def mock_sensor(test_state: list, device_number=0): - """Mock a tradfri sensor.""" - dev_info_mock = MagicMock() - dev_info_mock.manufacturer = "manufacturer" - dev_info_mock.model_number = "model" - dev_info_mock.firmware_version = "1.2.3" - - _mock_sensor = Mock( - id=f"mock-sensor-id-{device_number}", - reachable=True, - observe=Mock(), - device_info=dev_info_mock, - has_light_control=False, - has_socket_control=False, - has_blind_control=False, - has_signal_repeater_control=False, - has_air_purifier_control=False, - ) - - # Set state value, eg battery_level = 50, or has_air_purifier_control=True - for state in test_state: - setattr(dev_info_mock, state["attribute"], state["value"]) - - _mock_sensor.name = f"tradfri_sensor_{device_number}" - - return _mock_sensor - - -async def test_battery_sensor(hass, mock_gateway, mock_api_factory): +async def test_battery_sensor( + hass: HomeAssistant, + mock_gateway: Mock, + mock_api_factory: MagicMock, + remote_control: Device, +) -> None: """Test that a battery sensor is correctly added.""" - mock_gateway.mock_devices.append( - mock_sensor(test_state=[{"attribute": "battery_level", "value": 60}]) - ) + entity_id = "sensor.test" + device = remote_control + mock_gateway.mock_devices.append(device) await setup_integration(hass) - sensor_1 = hass.states.get("sensor.tradfri_sensor_0") - assert sensor_1 is not None - assert sensor_1.state == "60" - assert sensor_1.attributes["unit_of_measurement"] == "%" - assert sensor_1.attributes["device_class"] == "battery" + state = hass.states.get(entity_id) + assert state + assert state.state == "87" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + await trigger_observe_callback( + hass, mock_gateway, device, {ATTR_DEVICE_INFO: {ATTR_DEVICE_BATTERY: 60}} + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == "60" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT -async def test_cover_battery_sensor(hass, mock_gateway, mock_api_factory): +async def test_cover_battery_sensor( + hass: HomeAssistant, + mock_gateway: Mock, + mock_api_factory: MagicMock, + blind: Blind, +) -> None: """Test that a battery sensor is correctly added for a cover (blind).""" - mock_gateway.mock_devices.append( - mock_sensor( - test_state=[ - {"attribute": "battery_level", "value": 42, "has_blind_control": True} - ] - ) - ) + entity_id = "sensor.test" + device = blind.device + mock_gateway.mock_devices.append(device) await setup_integration(hass) - sensor_1 = hass.states.get("sensor.tradfri_sensor_0") - assert sensor_1 is not None - assert sensor_1.state == "42" - assert sensor_1.attributes["unit_of_measurement"] == "%" - assert sensor_1.attributes["device_class"] == "battery" - assert sensor_1.attributes["state_class"] == "measurement" + state = hass.states.get(entity_id) + assert state + assert state.state == "77" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT -async def test_air_quality_sensor(hass, mock_gateway, mock_api_factory): +async def test_air_quality_sensor( + hass: HomeAssistant, + mock_gateway: Mock, + mock_api_factory: MagicMock, + air_purifier: AirPurifier, +) -> None: """Test that a battery sensor is correctly added.""" - mock_gateway.mock_devices.append( - mock_fan( - test_state={ - "fan_speed": 10, - "air_quality": 42, - "filter_lifetime_remaining": 120, - } - ) - ) + entity_id = "sensor.test_air_quality" + device = air_purifier.device + mock_gateway.mock_devices.append(device) await setup_integration(hass) - sensor_1 = hass.states.get("sensor.tradfri_fan_0_air_quality") - assert sensor_1 is not None - assert sensor_1.state == "42" - assert sensor_1.attributes["unit_of_measurement"] == "µg/m³" - assert sensor_1.attributes["state_class"] == "measurement" - assert "device_class" not in sensor_1.attributes + state = hass.states.get(entity_id) + assert state + assert state.state == "5" + assert ( + state.attributes[ATTR_UNIT_OF_MEASUREMENT] + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + assert ATTR_DEVICE_CLASS not in state.attributes + + # The sensor returns 65535 if the fan is turned off + await trigger_observe_callback( + hass, + mock_gateway, + device, + {ROOT_AIR_PURIFIER: [{ATTR_AIR_PURIFIER_AIR_QUALITY: 65535}]}, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN -async def test_filter_time_left_sensor(hass, mock_gateway, mock_api_factory): +async def test_filter_time_left_sensor( + hass: HomeAssistant, + mock_gateway: Mock, + mock_api_factory: MagicMock, + air_purifier: AirPurifier, +) -> None: """Test that a battery sensor is correctly added.""" - mock_gateway.mock_devices.append( - mock_fan( - test_state={ - "fan_speed": 10, - "air_quality": 42, - "filter_lifetime_remaining": 120, - } - ) - ) + entity_id = "sensor.test_filter_time_left" + device = air_purifier.device + mock_gateway.mock_devices.append(device) await setup_integration(hass) - sensor_1 = hass.states.get("sensor.tradfri_fan_0_filter_time_left") - assert sensor_1 is not None - assert sensor_1.state == "2" - assert sensor_1.attributes["unit_of_measurement"] == "h" + state = hass.states.get(entity_id) + + assert state + assert state.state == "4320" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTime.HOURS + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT -async def test_sensor_observed(hass, mock_gateway, mock_api_factory): - """Test that sensors are correctly observed.""" - sensor = mock_sensor(test_state=[{"attribute": "battery_level", "value": 60}]) - mock_gateway.mock_devices.append(sensor) - await setup_integration(hass) - assert len(sensor.observe.mock_calls) > 0 - - -async def test_sensor_available(hass, mock_gateway, mock_api_factory): +async def test_sensor_available( + hass: HomeAssistant, + mock_gateway: Mock, + mock_api_factory: MagicMock, + air_purifier: AirPurifier, +) -> None: """Test sensor available property.""" - sensor = mock_sensor( - test_state=[{"attribute": "battery_level", "value": 60}], device_number=1 - ) - sensor.reachable = True - - sensor2 = mock_sensor( - test_state=[{"attribute": "battery_level", "value": 60}], device_number=2 - ) - sensor2.reachable = False - - mock_gateway.mock_devices.append(sensor) - mock_gateway.mock_devices.append(sensor2) + entity_id = "sensor.test_filter_time_left" + device = air_purifier.device + mock_gateway.mock_devices.append(device) await setup_integration(hass) - assert hass.states.get("sensor.tradfri_sensor_1").state == "60" - assert hass.states.get("sensor.tradfri_sensor_2").state == "unavailable" + state = hass.states.get(entity_id) + assert state + assert state.state == "4320" + + await trigger_observe_callback( + hass, mock_gateway, device, {ATTR_REACHABLE_STATE: 0} + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE -async def test_unique_id_migration(hass, mock_gateway, mock_api_factory): +async def test_unique_id_migration( + hass: HomeAssistant, + mock_gateway: Mock, + mock_api_factory: MagicMock, + remote_control: Device, +) -> None: """Test unique ID is migrated from old format to new.""" ent_reg = er.async_get(hass) - old_unique_id = f"{GATEWAY_ID}-mock-sensor-id-0" + old_unique_id = f"{GATEWAY_ID}-65536" entry = MockConfigEntry( - domain=tradfri.DOMAIN, + domain=DOMAIN, data={ "host": "mock-host", "identity": "mock-identity", @@ -208,32 +206,30 @@ async def test_unique_id_migration(hass, mock_gateway, mock_api_factory): entry.add_to_hass(hass) # Version 1 - sensor_name = "sensor.tradfri_sensor_0" - entity_name = sensor_name.split(".")[1] + entity_id = "sensor.test" + entity_name = entity_id.split(".")[1] entity_entry = ent_reg.async_get_or_create( - "sensor", - tradfri.DOMAIN, + SENSOR_DOMAIN, + DOMAIN, old_unique_id, suggested_object_id=entity_name, config_entry=entry, original_name=entity_name, ) - assert entity_entry.entity_id == sensor_name + assert entity_entry.entity_id == entity_id assert entity_entry.unique_id == old_unique_id # Add a sensor to the gateway so that it populates coordinator list - sensor = mock_sensor( - test_state=[{"attribute": "battery_level", "value": 60}], - ) - mock_gateway.mock_devices.append(sensor) - + device = remote_control + mock_gateway.mock_devices.append(device) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(sensor_name) - new_unique_id = f"{GATEWAY_ID}-mock-sensor-id-0-battery_level" - assert entity_entry.unique_id == new_unique_id - assert ent_reg.async_get_entity_id("sensor", tradfri.DOMAIN, old_unique_id) is None + new_unique_id = f"{old_unique_id}-battery_level" + migrated_entity_entry = ent_reg.async_get(entity_id) + assert migrated_entity_entry is not None + assert migrated_entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) is None