diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 18d62c9a194..32c4b74e8a5 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Generator -from unittest.mock import MagicMock, Mock, PropertyMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest @@ -79,19 +79,3 @@ def mock_api_factory(mock_api) -> Generator[MagicMock, None, None]: factory.init.return_value = factory.return_value factory.return_value.request = mock_api yield factory.return_value - - -@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 diff --git a/tests/components/tradfri/fixtures/air_purifier.json b/tests/components/tradfri/fixtures/air_purifier.json new file mode 100644 index 00000000000..4b243a71c50 --- /dev/null +++ b/tests/components/tradfri/fixtures/air_purifier.json @@ -0,0 +1,32 @@ +{ + "3": { + "0": "IKEA of Sweden", + "1": "STARKVIND Air purifier", + "2": "", + "3": "1.0.033", + "6": 1, + "7": 4364 + }, + "5750": 10, + "9001": "Test", + "9002": 1633096623, + "9003": 65551, + "9019": 1, + "9020": 1633096633, + "9054": 0, + "15025": [ + { + "5900": 0, + "5902": 2, + "5903": 0, + "5904": 259200, + "5905": 0, + "5906": 0, + "5907": 5, + "5908": 10, + "5909": 2, + "5910": 259198, + "9003": 0 + } + ] +} diff --git a/tests/components/tradfri/test_diagnostics.py b/tests/components/tradfri/test_diagnostics.py index c2b888f1c50..2b400cb39bb 100644 --- a/tests/components/tradfri/test_diagnostics.py +++ b/tests/components/tradfri/test_diagnostics.py @@ -1,15 +1,33 @@ """Tests for Tradfri diagnostics.""" -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock, Mock, PropertyMock, patch + +import pytest from homeassistant.core import HomeAssistant from .common import setup_integration -from .test_fan import mock_fan +from .test_sensor import mock_fan from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator +@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 + + async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -34,4 +52,4 @@ async def test_diagnostics( assert isinstance(result, dict) assert result["gateway_version"] == "1.2.1234" - assert len(result["device_data"]) == 1 + assert result["device_data"] == ["model"] diff --git a/tests/components/tradfri/test_fan.py b/tests/components/tradfri/test_fan.py index 63e6a6558c9..60a38e06f0c 100644 --- a/tests/components/tradfri/test_fan.py +++ b/tests/components/tradfri/test_fan.py @@ -1,144 +1,216 @@ """Tradfri fan (recognised as air purifiers 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_AIR_PURIFIER_FAN_SPEED, + ATTR_AIR_PURIFIER_MODE, + ATTR_REACHABLE_STATE, + ROOT_AIR_PURIFIER, +) from pytradfri.device import Device from pytradfri.device.air_purifier import AirPurifier -from pytradfri.device.air_purifier_control import AirPurifierControl -from .common import setup_integration +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, + SERVICE_SET_PRESET_MODE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.components.tradfri.const import DOMAIN +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from .common import setup_integration, trigger_observe_callback + +from tests.common import load_fixture -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(scope="module") +def air_purifier_response() -> dict[str, Any]: + """Return an air purifier response.""" + return json.loads(load_fixture("air_purifier.json", DOMAIN)) -async def test_fan(hass, mock_gateway, mock_api_factory): - """Test that fans are correctly added.""" - state = {"fan_speed": 10, "air_quality": 12} - - mock_gateway.mock_devices.append(mock_fan(test_state=state)) - await setup_integration(hass) - - fan_1 = hass.states.get("fan.tradfri_fan_0") - assert fan_1 is not None - assert fan_1.state == "on" - assert fan_1.attributes["percentage"] == 18 - assert fan_1.attributes["preset_modes"] == ["Auto"] - assert fan_1.attributes["supported_features"] == 9 +@pytest.fixture +def air_purifier(air_purifier_response: dict[str, Any]) -> AirPurifier: + """Return air purifier.""" + device = Device(air_purifier_response) + air_purifier_control = device.air_purifier_control + assert air_purifier_control + return air_purifier_control.air_purifiers[0] -async def test_fan_observed(hass, mock_gateway, mock_api_factory): - """Test that fans are correctly observed.""" - state = {"fan_speed": 10, "air_quality": 12} - - fan = mock_fan(test_state=state) - mock_gateway.mock_devices.append(fan) - await setup_integration(hass) - assert len(fan.observe.mock_calls) > 0 - - -async def test_fan_available(hass, mock_gateway, mock_api_factory): +async def test_fan_available( + hass: HomeAssistant, + mock_gateway: Mock, + mock_api_factory: MagicMock, + air_purifier: AirPurifier, +) -> None: """Test fan available property.""" - - fan = mock_fan(test_state={"fan_speed": 10, "air_quality": 12}, device_number=1) - fan.reachable = True - - fan2 = mock_fan(test_state={"fan_speed": 10, "air_quality": 12}, device_number=2) - fan2.reachable = False - - mock_gateway.mock_devices.append(fan) - mock_gateway.mock_devices.append(fan2) + entity_id = "fan.test" + device = air_purifier.device + mock_gateway.mock_devices.append(device) await setup_integration(hass) - assert hass.states.get("fan.tradfri_fan_1").state == "on" - assert hass.states.get("fan.tradfri_fan_2").state == "unavailable" + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_PERCENTAGE] == 18 + assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(2.040816) + assert state.attributes[ATTR_PRESET_MODES] == ["Auto"] + assert state.attributes[ATTR_PRESET_MODE] is None + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 9 + + 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 @pytest.mark.parametrize( - "test_data, expected_result", + ( + "service, service_data, device_state, expected_state, " + "expected_percentage, expected_preset_mode" + ), [ ( - {"percentage": 50}, - "on", + SERVICE_SET_PERCENTAGE, + {ATTR_PERCENTAGE: 50}, + { + ATTR_AIR_PURIFIER_FAN_SPEED: 25, + ATTR_AIR_PURIFIER_MODE: 25, + }, + STATE_ON, + 49, + None, + ), + ( + SERVICE_SET_PERCENTAGE, + {ATTR_PERCENTAGE: 0}, + { + ATTR_AIR_PURIFIER_FAN_SPEED: 0, + ATTR_AIR_PURIFIER_MODE: 0, + }, + STATE_OFF, + None, + None, + ), + ( + SERVICE_TURN_ON, + {ATTR_PERCENTAGE: 50}, + { + ATTR_AIR_PURIFIER_FAN_SPEED: 25, + ATTR_AIR_PURIFIER_MODE: 25, + }, + STATE_ON, + 49, + None, + ), + ( + SERVICE_TURN_ON, + {ATTR_PRESET_MODE: "Auto"}, + { + ATTR_AIR_PURIFIER_MODE: 1, + }, + STATE_ON, + 18, + "Auto", + ), + ( + SERVICE_TURN_ON, + {}, + { + ATTR_AIR_PURIFIER_MODE: 1, + }, + STATE_ON, + 18, + "Auto", + ), + ( + SERVICE_SET_PRESET_MODE, + {ATTR_PRESET_MODE: "Auto"}, + { + ATTR_AIR_PURIFIER_MODE: 1, + }, + STATE_ON, + 18, + "Auto", + ), + ( + SERVICE_TURN_OFF, + {}, + { + ATTR_AIR_PURIFIER_FAN_SPEED: 0, + ATTR_AIR_PURIFIER_MODE: 0, + }, + STATE_OFF, + None, + None, ), - ({"percentage": 0}, "off"), ], ) -async def test_set_percentage( - hass, - mock_gateway, - mock_api_factory, - test_data, - expected_result, -): - """Test setting speed of a fan.""" - # Note pytradfri style, not hass. Values not really important. - initial_state = {"percentage": 10, "fan_speed": 3, "air_quality": 12} - # Setup the gateway with a mock fan. - fan = mock_fan(test_state=initial_state, device_number=0) - mock_gateway.mock_devices.append(fan) +async def test_services( + hass: HomeAssistant, + mock_gateway: Mock, + mock_api_factory: MagicMock, + air_purifier: AirPurifier, + service: str, + service_data: dict[str, Any], + device_state: dict[str, Any], + expected_state: str, + expected_percentage: int | None, + expected_preset_mode: str | None, +) -> None: + """Test fan services.""" + entity_id = "fan.test" + device = air_purifier.device + mock_gateway.mock_devices.append(device) await setup_integration(hass) - # Use the turn_on service call to change the fan state. + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_PERCENTAGE] == 18 + assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(2.040816) + assert state.attributes[ATTR_PRESET_MODES] == ["Auto"] + assert state.attributes[ATTR_PRESET_MODE] is None + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 9 + await hass.services.async_call( - "fan", - "set_percentage", - {"entity_id": "fan.tradfri_fan_0", **test_data}, + FAN_DOMAIN, + service, + {"entity_id": entity_id, **service_data}, blocking=True, ) await hass.async_block_till_done() - # Check that the fan is observed. - mock_func = fan.observe - assert len(mock_func.mock_calls) > 0 - _, callkwargs = mock_func.call_args - assert "callback" in callkwargs - # Callback function to refresh fan state. - callback = callkwargs["callback"] + await trigger_observe_callback( + hass, + mock_gateway, + device, + {ROOT_AIR_PURIFIER: [device_state]}, + ) - responses = mock_gateway.mock_responses - mock_gateway_response = responses[0] - - # A KeyError is raised if we don't this to the response code - mock_gateway_response["15025"][0].update({"5908": 10, "5907": 12, "5910": 20}) - - # Use the callback function to update the fan state. - dev = Device(mock_gateway_response) - fan_data = AirPurifier(dev, 0) - fan.air_purifier_control.air_purifiers[0] = fan_data - callback(fan) - await hass.async_block_till_done() - - # Check that the state is correct. - state = hass.states.get("fan.tradfri_fan_0") - assert state.state == expected_result + state = hass.states.get(entity_id) + assert state + assert state.state == expected_state + assert state.attributes[ATTR_PERCENTAGE] == expected_percentage + assert state.attributes[ATTR_PRESET_MODE] == expected_preset_mode diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py index 10904b8ffa6..25b6acea0de 100644 --- a/tests/components/tradfri/test_sensor.py +++ b/tests/components/tradfri/test_sensor.py @@ -1,18 +1,68 @@ """Tradfri sensor platform tests.""" from __future__ import annotations -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock, Mock, PropertyMock, patch + +import pytest +from pytradfri.device.air_purifier_control import AirPurifierControl from homeassistant.components import tradfri from homeassistant.helpers import entity_registry as er from . import GATEWAY_ID from .common import setup_integration -from .test_fan import mock_fan from tests.common import MockConfigEntry +@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 + + +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 + + def mock_sensor(test_state: list, device_number=0): """Mock a tradfri sensor.""" dev_info_mock = MagicMock()