From 5de863979811b57923753a6f36dbd1b12af29a08 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Fri, 22 Jan 2021 02:43:52 +0800 Subject: [PATCH] Rewrite dyson sensor test (#45382) --- homeassistant/components/dyson/sensor.py | 104 +++-- tests/components/dyson/common.py | 26 +- tests/components/dyson/conftest.py | 26 +- tests/components/dyson/test_sensor.py | 485 ++++++++--------------- 4 files changed, 236 insertions(+), 405 deletions(-) diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index b7235bd1797..f1198188b5c 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -5,6 +5,9 @@ from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -16,37 +19,41 @@ from homeassistant.helpers.entity import Entity from . import DYSON_DEVICES, DysonEntity -SENSOR_UNITS = { - "filter_life": TIME_HOURS, - "carbon_filter_state": PERCENTAGE, - "hepa_filter_state": PERCENTAGE, - "combi_filter_state": PERCENTAGE, - "humidity": PERCENTAGE, -} - -SENSOR_ICONS = { - "air_quality": "mdi:fan", - "dust": "mdi:cloud", - "filter_life": "mdi:filter-outline", - "carbon_filter_state": "mdi:filter-outline", - "hepa_filter_state": "mdi:filter-outline", - "combi_filter_state": "mdi:filter-outline", +SENSOR_ATTRIBUTES = { + "air_quality": {ATTR_ICON: "mdi:fan"}, + "dust": {ATTR_ICON: "mdi:cloud"}, + "humidity": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + "temperature": {ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE}, + "filter_life": { + ATTR_ICON: "mdi:filter-outline", + ATTR_UNIT_OF_MEASUREMENT: TIME_HOURS, + }, + "carbon_filter_state": { + ATTR_ICON: "mdi:filter-outline", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + "combi_filter_state": { + ATTR_ICON: "mdi:filter-outline", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + "hepa_filter_state": { + ATTR_ICON: "mdi:filter-outline", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, } SENSOR_NAMES = { "air_quality": "AQI", "dust": "Dust", - "filter_life": "Filter Life", "humidity": "Humidity", - "carbon_filter_state": "Carbon Filter Remaining Life", - "hepa_filter_state": "HEPA Filter Remaining Life", - "combi_filter_state": "Combi Filter Remaining Life", "temperature": "Temperature", -} - -SENSOR_DEVICE_CLASSES = { - "humidity": DEVICE_CLASS_HUMIDITY, - "temperature": DEVICE_CLASS_TEMPERATURE, + "filter_life": "Filter Life", + "carbon_filter_state": "Carbon Filter Remaining Life", + "combi_filter_state": "Combi Filter Remaining Life", + "hepa_filter_state": "HEPA Filter Remaining Life", } DYSON_SENSOR_DEVICES = "dyson_sensor_devices" @@ -106,6 +113,7 @@ class DysonSensor(DysonEntity, Entity): super().__init__(device, None) self._old_value = None self._sensor_type = sensor_type + self._attributes = SENSOR_ATTRIBUTES[sensor_type] def on_message(self, message): """Handle new messages which are received from the fan.""" @@ -127,17 +135,17 @@ class DysonSensor(DysonEntity, Entity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return SENSOR_UNITS.get(self._sensor_type) + return self._attributes.get(ATTR_UNIT_OF_MEASUREMENT) @property def icon(self): """Return the icon for this sensor.""" - return SENSOR_ICONS.get(self._sensor_type) + return self._attributes.get(ATTR_ICON) @property def device_class(self): """Return the device class of this sensor.""" - return SENSOR_DEVICE_CLASSES.get(self._sensor_type) + return self._attributes.get(ATTR_DEVICE_CLASS) class DysonFilterLifeSensor(DysonSensor): @@ -150,9 +158,7 @@ class DysonFilterLifeSensor(DysonSensor): @property def state(self): """Return filter life in hours.""" - if self._device.state: - return int(self._device.state.filter_life) - return None + return int(self._device.state.filter_life) class DysonCarbonFilterLifeSensor(DysonSensor): @@ -165,9 +171,7 @@ class DysonCarbonFilterLifeSensor(DysonSensor): @property def state(self): """Return filter life remaining in percent.""" - if self._device.state: - return int(self._device.state.carbon_filter_state) - return None + return int(self._device.state.carbon_filter_state) class DysonHepaFilterLifeSensor(DysonSensor): @@ -180,9 +184,7 @@ class DysonHepaFilterLifeSensor(DysonSensor): @property def state(self): """Return filter life remaining in percent.""" - if self._device.state: - return int(self._device.state.hepa_filter_state) - return None + return int(self._device.state.hepa_filter_state) class DysonDustSensor(DysonSensor): @@ -195,9 +197,7 @@ class DysonDustSensor(DysonSensor): @property def state(self): """Return Dust value.""" - if self._device.environmental_state: - return self._device.environmental_state.dust - return None + return self._device.environmental_state.dust class DysonHumiditySensor(DysonSensor): @@ -210,11 +210,9 @@ class DysonHumiditySensor(DysonSensor): @property def state(self): """Return Humidity value.""" - if self._device.environmental_state: - if self._device.environmental_state.humidity == 0: - return STATE_OFF - return self._device.environmental_state.humidity - return None + if self._device.environmental_state.humidity == 0: + return STATE_OFF + return self._device.environmental_state.humidity class DysonTemperatureSensor(DysonSensor): @@ -228,14 +226,12 @@ class DysonTemperatureSensor(DysonSensor): @property def state(self): """Return Temperature value.""" - if self._device.environmental_state: - temperature_kelvin = self._device.environmental_state.temperature - if temperature_kelvin == 0: - return STATE_OFF - if self._unit == TEMP_CELSIUS: - return float(f"{(temperature_kelvin - 273.15):.1f}") - return float(f"{(temperature_kelvin * 9 / 5 - 459.67):.1f}") - return None + temperature_kelvin = self._device.environmental_state.temperature + if temperature_kelvin == 0: + return STATE_OFF + if self._unit == TEMP_CELSIUS: + return float(f"{(temperature_kelvin - 273.15):.1f}") + return float(f"{(temperature_kelvin * 9 / 5 - 459.67):.1f}") @property def unit_of_measurement(self): @@ -253,6 +249,4 @@ class DysonAirQualitySensor(DysonSensor): @property def state(self): """Return Air Quality value.""" - if self._device.environmental_state: - return int(self._device.environmental_state.volatil_organic_compounds) - return None + return int(self._device.environmental_state.volatil_organic_compounds) diff --git a/tests/components/dyson/common.py b/tests/components/dyson/common.py index be6376b9c29..5f2c345bed3 100644 --- a/tests/components/dyson/common.py +++ b/tests/components/dyson/common.py @@ -7,6 +7,8 @@ from unittest.mock import MagicMock from libpurecool.dyson_device import DysonDevice from libpurecool.dyson_pure_cool import FanSpeed +from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant SERIAL = "XX-XXXXX-XX" @@ -15,6 +17,20 @@ ENTITY_NAME = "temp_name" BASE_PATH = "homeassistant.components.dyson" +CONFIG = { + DOMAIN: { + CONF_USERNAME: "user@example.com", + CONF_PASSWORD: "password", + CONF_LANGUAGE: "US", + CONF_DEVICES: [ + { + "device_id": SERIAL, + "device_ip": "0.0.0.0", + } + ], + } +} + def load_mock_device(device: DysonDevice) -> None: """Load the mock with default values so it doesn't throw errors.""" @@ -49,7 +65,13 @@ async def async_update_device( hass: HomeAssistant, device: DysonDevice, state_type: Optional[Type] = None ) -> None: """Update the device using callback function.""" - callback = device.add_message_listener.call_args[0][0] + callbacks = [args[0][0] for args in device.add_message_listener.call_args_list] message = MagicMock(spec=state_type) - await hass.async_add_executor_job(callback, message) + + # Combining sync calls to avoid multiple executors + def _run_callbacks(): + for callback in callbacks: + callback(message) + + await hass.async_add_executor_job(_run_callbacks) await hass.async_block_till_done() diff --git a/tests/components/dyson/conftest.py b/tests/components/dyson/conftest.py index f3e4c3fc72c..b1ab89d7b3b 100644 --- a/tests/components/dyson/conftest.py +++ b/tests/components/dyson/conftest.py @@ -4,16 +4,13 @@ from unittest.mock import patch from libpurecool.dyson_device import DysonDevice import pytest -from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN -from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.dyson import DOMAIN from homeassistant.core import HomeAssistant -from .common import SERIAL +from .common import BASE_PATH, CONFIG from tests.common import async_setup_component -BASE_PATH = "homeassistant.components.dyson" - @pytest.fixture() async def device(hass: HomeAssistant, request) -> DysonDevice: @@ -21,7 +18,10 @@ async def device(hass: HomeAssistant, request) -> DysonDevice: platform = request.module.PLATFORM_DOMAIN get_device = request.module.get_device if hasattr(request, "param"): - device = get_device(request.param) + if isinstance(request.param, list): + device = get_device(*request.param) + else: + device = get_device(request.param) else: device = get_device() with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( @@ -31,19 +31,7 @@ async def device(hass: HomeAssistant, request) -> DysonDevice: await async_setup_component( hass, DOMAIN, - { - DOMAIN: { - CONF_USERNAME: "user@example.com", - CONF_PASSWORD: "password", - CONF_LANGUAGE: "US", - CONF_DEVICES: [ - { - "device_id": SERIAL, - "device_ip": "0.0.0.0", - } - ], - } - }, + CONFIG, ) await hass.async_block_till_done() diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 0d2fbaf75fd..06daa94ce97 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -1,355 +1,182 @@ """Test the Dyson sensor(s) component.""" -import unittest -from unittest import mock +from typing import List, Type from unittest.mock import patch from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +import pytest -from homeassistant.components import dyson as dyson_parent -from homeassistant.components.dyson import sensor as dyson +from homeassistant.components.dyson import DOMAIN +from homeassistant.components.dyson.sensor import SENSOR_ATTRIBUTES, SENSOR_NAMES +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_TEMPERATURE, - PERCENTAGE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, - TIME_HOURS, ) -from homeassistant.helpers import discovery -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem -from .common import load_mock_device +from .common import ( + BASE_PATH, + CONFIG, + ENTITY_NAME, + NAME, + SERIAL, + async_update_device, + get_basic_device, +) -from tests.common import get_test_home_assistant +from tests.common import async_setup_component + +ENTITY_ID_PREFIX = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" + +MOCKED_VALUES = { + "filter_life": 100, + "dust": 5, + "humidity": 45, + "temperature_kelvin": 295, + "temperature": 21.9, + "air_quality": 5, + "hepa_filter_state": 50, + "combi_filter_state": 50, + "carbon_filter_state": 10, +} + +MOCKED_UPDATED_VALUES = { + "filter_life": 30, + "dust": 2, + "humidity": 80, + "temperature_kelvin": 240, + "temperature": -33.1, + "air_quality": 3, + "hepa_filter_state": 30, + "combi_filter_state": 30, + "carbon_filter_state": 20, +} -def _get_dyson_purecool_device(): - """Return a valid device provide by Dyson web services.""" - device = mock.Mock(spec=DysonPureCool) - load_mock_device(device) - return device - - -def _get_config(): - """Return a config dictionary.""" - return { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "GB", - dyson_parent.CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"} - ], - } - } - - -def _get_device_without_state(): - """Return a valid device provide by Dyson web services.""" - device = mock.Mock(spec=DysonPureCoolLink) - device.name = "Device_name" - device.state = None - device.environmental_state = None - return device - - -def _get_with_state(): - """Return a valid device with state values.""" - device = mock.Mock() - load_mock_device(device) - device.name = "Device_name" - device.state.filter_life = 100 - device.environmental_state.dust = 5 - device.environmental_state.humidity = 45 - device.environmental_state.temperature = 295 - device.environmental_state.volatil_organic_compounds = 2 - - return device - - -def _get_purecool_device(): - """Return a valid device with filters life state values.""" - device = mock.Mock(spec=DysonPureCool) - load_mock_device(device) - device.name = "PureCool" - device.state.carbon_filter_state = "0096" - device.state.hepa_filter_state = "0056" - device.environmental_state.dust = 5 - device.environmental_state.humidity = 45 - device.environmental_state.temperature = 295 - device.environmental_state.volatil_organic_compounds = 2 - - return device - - -def _get_purecool_humidify_device(): - """Return a valid device with filters life state values.""" - device = mock.Mock(spec=DysonPureCool) - load_mock_device(device) - device.name = "PureCool_Humidify" - device.state.carbon_filter_state = "INV" - device.state.hepa_filter_state = "0075" - device.environmental_state.dust = 5 - device.environmental_state.humidity = 45 - device.environmental_state.temperature = 295 - device.environmental_state.volatil_organic_compounds = 2 - - return device - - -def _get_with_standby_monitoring(): - """Return a valid device with state but with standby monitoring disable.""" - device = mock.Mock() - load_mock_device(device) - device.name = "Device_name" - device.environmental_state.humidity = 0 - device.environmental_state.temperature = 0 - - return device - - -class DysonTest(unittest.TestCase): - """Dyson Sensor component test class.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component_with_no_devices(self): - """Test setup component with no devices.""" - self.hass.data[dyson.DYSON_DEVICES] = [] - add_entities = mock.MagicMock() - dyson.setup_platform(self.hass, None, add_entities) - add_entities.assert_not_called() - - def test_setup_component(self): - """Test setup component with devices.""" - - def _add_device(devices): - assert len(devices) == 5 - assert devices[0].name == "Device_name Filter Life" - assert devices[1].name == "Device_name Dust" - assert devices[2].name == "Device_name Humidity" - assert devices[3].name == "Device_name Temperature" - assert devices[4].name == "Device_name AQI" - - device_fan = _get_device_without_state() - device_non_fan = _get_with_state() - self.hass.data[dyson.DYSON_DEVICES] = [ - device_fan, - device_non_fan, - ] - dyson.setup_platform(self.hass, None, _add_device, mock.MagicMock()) - - def test_dyson_filter_life_sensor(self): - """Test filter life sensor with no value.""" - sensor = dyson.DysonFilterLifeSensor(_get_device_without_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement == TIME_HOURS - assert sensor.name == "Device_name Filter Life" - assert sensor.entity_id == "sensor.dyson_1" - sensor.on_message("message") - - def test_dyson_filter_life_sensor_with_values(self): - """Test filter sensor with values.""" - sensor = dyson.DysonFilterLifeSensor(_get_with_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 100 - assert sensor.unit_of_measurement == TIME_HOURS - assert sensor.name == "Device_name Filter Life" - assert sensor.entity_id == "sensor.dyson_1" - sensor.on_message("message") - - def test_dyson_dust_sensor(self): - """Test dust sensor with no value.""" - sensor = dyson.DysonDustSensor(_get_device_without_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement is None - assert sensor.name == "Device_name Dust" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_dust_sensor_with_values(self): - """Test dust sensor with values.""" - sensor = dyson.DysonDustSensor(_get_with_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 5 - assert sensor.unit_of_measurement is None - assert sensor.name == "Device_name Dust" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_humidity_sensor(self): - """Test humidity sensor with no value.""" - sensor = dyson.DysonHumiditySensor(_get_device_without_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement == PERCENTAGE - assert sensor.name == "Device_name Humidity" - assert sensor.entity_id == "sensor.dyson_1" - assert sensor.device_class == DEVICE_CLASS_HUMIDITY - - def test_dyson_humidity_sensor_with_values(self): - """Test humidity sensor with values.""" - sensor = dyson.DysonHumiditySensor(_get_with_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 45 - assert sensor.unit_of_measurement == PERCENTAGE - assert sensor.name == "Device_name Humidity" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_humidity_standby_monitoring(self): - """Test humidity sensor while device is in standby monitoring.""" - sensor = dyson.DysonHumiditySensor(_get_with_standby_monitoring()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == STATE_OFF - assert sensor.unit_of_measurement == PERCENTAGE - assert sensor.name == "Device_name Humidity" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_temperature_sensor(self): - """Test temperature sensor with no value.""" - sensor = dyson.DysonTemperatureSensor(_get_device_without_state(), TEMP_CELSIUS) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement == TEMP_CELSIUS - assert sensor.name == "Device_name Temperature" - assert sensor.entity_id == "sensor.dyson_1" - assert sensor.device_class == DEVICE_CLASS_TEMPERATURE - - def test_dyson_temperature_sensor_with_values(self): - """Test temperature sensor with values.""" - sensor = dyson.DysonTemperatureSensor(_get_with_state(), TEMP_CELSIUS) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 21.9 - assert sensor.unit_of_measurement == TEMP_CELSIUS - assert sensor.name == "Device_name Temperature" - assert sensor.entity_id == "sensor.dyson_1" - - sensor = dyson.DysonTemperatureSensor(_get_with_state(), TEMP_FAHRENHEIT) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 71.3 - assert sensor.unit_of_measurement == TEMP_FAHRENHEIT - assert sensor.name == "Device_name Temperature" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_temperature_standby_monitoring(self): - """Test temperature sensor while device is in standby monitoring.""" - sensor = dyson.DysonTemperatureSensor( - _get_with_standby_monitoring(), TEMP_CELSIUS +@callback +def _async_assign_values( + device: DysonPureCoolLink, values=MOCKED_VALUES, combi=False +) -> None: + """Assign mocked values to the device.""" + if isinstance(device, DysonPureCool): + device.state.hepa_filter_state = values["hepa_filter_state"] + device.state.carbon_filter_state = ( + "INV" if combi else values["carbon_filter_state"] ) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == STATE_OFF - assert sensor.unit_of_measurement == TEMP_CELSIUS - assert sensor.name == "Device_name Temperature" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_air_quality_sensor(self): - """Test air quality sensor with no value.""" - sensor = dyson.DysonAirQualitySensor(_get_device_without_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement is None - assert sensor.name == "Device_name AQI" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_air_quality_sensor_with_values(self): - """Test air quality sensor with values.""" - sensor = dyson.DysonAirQualitySensor(_get_with_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 2 - assert sensor.unit_of_measurement is None - assert sensor.name == "Device_name AQI" - assert sensor.entity_id == "sensor.dyson_1" + device.environmental_state.humidity = values["humidity"] + device.environmental_state.temperature = values["temperature_kelvin"] + else: # DysonPureCoolLink + device.state.filter_life = values["filter_life"] + device.environmental_state.dust = values["dust"] + device.environmental_state.humidity = values["humidity"] + device.environmental_state.temperature = values["temperature_kelvin"] + device.environmental_state.volatil_organic_compounds = values["air_quality"] -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], +@callback +def get_device(spec: Type[DysonPureCoolLink], combi=False) -> DysonPureCoolLink: + """Return a device of the given type.""" + device = get_basic_device(spec) + _async_assign_values(device, combi=combi) + return device + + +@callback +def _async_get_entity_id(sensor_type: str) -> str: + """Get the expected entity id from the type of the sensor.""" + sensor_name = SENSOR_NAMES[sensor_type] + entity_id_suffix = sensor_name.lower().replace(" ", "_") + return f"{ENTITY_ID_PREFIX}_{entity_id_suffix}" + + +@pytest.mark.parametrize( + "device,sensors", + [ + ( + DysonPureCoolLink, + ["filter_life", "dust", "humidity", "temperature", "air_quality"], + ), + ( + DysonPureCool, + ["hepa_filter_state", "carbon_filter_state", "humidity", "temperature"], + ), + ( + [DysonPureCool, True], + ["combi_filter_state", "humidity", "temperature"], + ), + ], + indirect=["device"], ) -async def test_purecool_component_setup_only_once(devices, login, hass): - """Test if entities are created only once.""" - config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) - await hass.async_block_till_done() - discovery.load_platform(hass, "sensor", dyson_parent.DOMAIN, {}, config) - await hass.async_block_till_done() +async def test_sensors( + hass: HomeAssistant, device: DysonPureCoolLink, sensors: List[str] +) -> None: + """Test the sensors.""" + # Temperature is given by the device in kelvin + # Make sure no other sensors are set up + assert len(hass.states.async_all()) == len(sensors) - assert len(hass.data[dyson.DYSON_SENSOR_DEVICES]) == 4 + er = await entity_registry.async_get_registry(hass) + for sensor in sensors: + entity_id = _async_get_entity_id(sensor) + + # Test unique id + assert er.async_get(entity_id).unique_id == f"{SERIAL}-{sensor}" + + # Test state + state = hass.states.get(entity_id) + assert state.state == str(MOCKED_VALUES[sensor]) + assert state.name == f"{NAME} {SENSOR_NAMES[sensor]}" + + # Test attributes + attributes = state.attributes + for attr, value in SENSOR_ATTRIBUTES[sensor].items(): + assert attributes[attr] == value + + # Test data update + _async_assign_values(device, MOCKED_UPDATED_VALUES) + await async_update_device(hass, device) + for sensor in sensors: + state = hass.states.get(_async_get_entity_id(sensor)) + assert state.state == str(MOCKED_UPDATED_VALUES[sensor]) -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_purecool_device()], +@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) +async def test_sensors_off(hass: HomeAssistant, device: DysonPureCoolLink) -> None: + """Test the case where temperature and humidity are not available.""" + device.environmental_state.temperature = 0 + device.environmental_state.humidity = 0 + await async_update_device(hass, device) + assert hass.states.get(f"{ENTITY_ID_PREFIX}_temperature").state == STATE_OFF + assert hass.states.get(f"{ENTITY_ID_PREFIX}_humidity").state == STATE_OFF + + +@pytest.mark.parametrize( + "unit_system,temp_unit,temperature", + [(METRIC_SYSTEM, TEMP_CELSIUS, 21.9), (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, 71.3)], ) -async def test_dyson_purecool_filter_state_sensor(devices, login, hass): - """Test filter sensor with values.""" - config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) - await hass.async_block_till_done() +async def test_temperature( + hass: HomeAssistant, unit_system: UnitSystem, temp_unit: str, temperature: float +) -> None: + """Test the temperature sensor in different units.""" + hass.config.units = unit_system - state = hass.states.get("sensor.purecool_hepa_filter_remaining_life") - assert state is not None - assert state.state == "56" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert state.name == "PureCool HEPA Filter Remaining Life" + device = get_device(DysonPureCoolLink) + with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( + f"{BASE_PATH}.DysonAccount.devices", return_value=[device] + ), patch(f"{BASE_PATH}.DYSON_PLATFORMS", [PLATFORM_DOMAIN]): + # DYSON_PLATFORMS is patched so that only the platform being tested is set up + await async_setup_component( + hass, + DOMAIN, + CONFIG, + ) + await hass.async_block_till_done() - state = hass.states.get("sensor.purecool_carbon_filter_remaining_life") - assert state is not None - assert state.state == "96" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert state.name == "PureCool Carbon Filter Remaining Life" - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_purecool_humidify_device()], -) -async def test_dyson_purecool_humidify_filter_state_sensor(devices, login, hass): - """Test filter sensor with values.""" - config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) - await hass.async_block_till_done() - - state = hass.states.get("sensor.purecool_humidify_combi_filter_remaining_life") - assert state is not None - assert state.state == "75" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert state.name == "PureCool_Humidify Combi Filter Remaining Life" + state = hass.states.get(f"{ENTITY_ID_PREFIX}_temperature") + assert state.state == str(temperature) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == temp_unit