diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index 6b2d7cbe74c..b17b510e93f 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -1,19 +1,36 @@ """Support for Dyson Pure Hot+Cool link fan.""" import logging -from libpurecool.const import FocusMode, HeatMode, HeatState, HeatTarget +from libpurecool.const import ( + FanPower, + FanSpeed, + FanState, + FocusMode, + HeatMode, + HeatState, + HeatTarget, +) +from libpurecool.dyson_pure_hotcool import DysonPureHotCool from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink from libpurecool.dyson_pure_state import DysonPureHotCoolState +from libpurecool.dyson_pure_state_v2 import DysonPureHotCoolV2State from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -24,26 +41,53 @@ from . import DYSON_DEVICES _LOGGER = logging.getLogger(__name__) SUPPORT_FAN = [FAN_FOCUS, FAN_DIFFUSE] +SUPPORT_FAN_PCOOL = [FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] SUPPORT_HVAG = [HVAC_MODE_COOL, HVAC_MODE_HEAT] +SUPPORT_HVAC_PCOOL = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE +DYSON_KNOWN_CLIMATE_DEVICES = "dyson_known_climate_devices" -def setup_platform(hass, config, add_devices, discovery_info=None): +SPEED_MAP = { + FanSpeed.FAN_SPEED_1.value: FAN_LOW, + FanSpeed.FAN_SPEED_2.value: FAN_LOW, + FanSpeed.FAN_SPEED_3.value: FAN_LOW, + FanSpeed.FAN_SPEED_4.value: FAN_LOW, + FanSpeed.FAN_SPEED_AUTO.value: FAN_AUTO, + FanSpeed.FAN_SPEED_5.value: FAN_MEDIUM, + FanSpeed.FAN_SPEED_6.value: FAN_MEDIUM, + FanSpeed.FAN_SPEED_7.value: FAN_MEDIUM, + FanSpeed.FAN_SPEED_8.value: FAN_HIGH, + FanSpeed.FAN_SPEED_9.value: FAN_HIGH, + FanSpeed.FAN_SPEED_10.value: FAN_HIGH, +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson fan components.""" if discovery_info is None: return - # Get Dyson Devices from parent component. - add_devices( - [ - DysonPureHotCoolLinkDevice(device) - for device in hass.data[DYSON_DEVICES] - if isinstance(device, DysonPureHotCoolLink) - ] - ) + known_devices = hass.data.setdefault(DYSON_KNOWN_CLIMATE_DEVICES, set()) + + # Get Dyson Devices from parent component + new_entities = [] + + for device in hass.data[DYSON_DEVICES]: + if device.serial not in known_devices: + if isinstance(device, DysonPureHotCool): + dyson_entity = DysonPureHotCoolEntity(device) + new_entities.append(dyson_entity) + known_devices.add(device.serial) + elif isinstance(device, DysonPureHotCoolLink): + dyson_entity = DysonPureHotCoolLinkEntity(device) + new_entities.append(dyson_entity) + known_devices.add(device.serial) + + add_entities(new_entities) -class DysonPureHotCoolLinkDevice(ClimateEntity): +class DysonPureHotCoolLinkEntity(ClimateEntity): """Representation of a Dyson climate fan.""" def __init__(self, device): @@ -57,11 +101,11 @@ class DysonPureHotCoolLinkDevice(ClimateEntity): def on_message(self, message): """Call when new messages received from the climate.""" - if not isinstance(message, DysonPureHotCoolState): - return - - _LOGGER.debug("Message received for climate device %s : %s", self.name, message) - self.schedule_update_ha_state() + if isinstance(message, DysonPureHotCoolState): + _LOGGER.debug( + "Message received for climate device %s : %s", self.name, message + ) + self.schedule_update_ha_state() @property def should_poll(self): @@ -188,3 +232,164 @@ class DysonPureHotCoolLinkDevice(ClimateEntity): def max_temp(self): """Return the maximum temperature.""" return 37 + + +class DysonPureHotCoolEntity(ClimateEntity): + """Representation of a Dyson climate hot+cool fan.""" + + def __init__(self, device): + """Initialize the fan.""" + self._device = device + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self.hass.async_add_executor_job( + self._device.add_message_listener, self.on_message + ) + + def on_message(self, message): + """Call when new messages received from the climate device.""" + if isinstance(message, DysonPureHotCoolV2State): + _LOGGER.debug( + "Message received for climate device %s : %s", self.name, message + ) + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def name(self): + """Return the display name of this climate.""" + return self._device.name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._device.environmental_state.temperature is not None: + temperature_kelvin = self._device.environmental_state.temperature + if temperature_kelvin != 0: + return float("{:.1f}".format(temperature_kelvin - 273)) + return None + + @property + def target_temperature(self): + """Return the target temperature.""" + heat_target = int(self._device.state.heat_target) / 10 + return int(heat_target - 273) + + @property + def current_humidity(self): + """Return the current humidity.""" + if self._device.environmental_state.humidity is not None: + if self._device.environmental_state.humidity != 0: + return self._device.environmental_state.humidity + return None + + @property + def hvac_mode(self): + """Return hvac operation ie. heat, cool mode. + + Need to be one of HVAC_MODE_*. + """ + if self._device.state.fan_power == FanPower.POWER_OFF.value: + return HVAC_MODE_OFF + if self._device.state.heat_mode == HeatMode.HEAT_ON.value: + return HVAC_MODE_HEAT + return HVAC_MODE_COOL + + @property + def hvac_modes(self): + """Return the list of available hvac operation modes. + + Need to be a subset of HVAC_MODES. + """ + return SUPPORT_HVAC_PCOOL + + @property + def hvac_action(self): + """Return the current running hvac operation if supported. + + Need to be one of CURRENT_HVAC_*. + """ + if self._device.state.fan_power == FanPower.POWER_OFF.value: + return CURRENT_HVAC_OFF + if self._device.state.heat_mode == HeatMode.HEAT_ON.value: + if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_COOL + + @property + def fan_mode(self): + """Return the fan setting.""" + if self._device.state.fan_state == FanState.FAN_OFF.value: + return FAN_OFF + + return SPEED_MAP[self._device.state.speed] + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return SUPPORT_FAN_PCOOL + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + _LOGGER.error("Missing target temperature %s", kwargs) + return + target_temp = int(target_temp) + _LOGGER.debug("Set %s temperature %s", self.name, target_temp) + # Limit the target temperature into acceptable range. + target_temp = min(self.max_temp, target_temp) + target_temp = max(self.min_temp, target_temp) + self._device.set_heat_target(HeatTarget.celsius(target_temp)) + + def set_fan_mode(self, fan_mode): + """Set new fan mode.""" + _LOGGER.debug("Set %s focus mode %s", self.name, fan_mode) + if fan_mode == FAN_OFF: + self._device.turn_off() + elif fan_mode == FAN_LOW: + self._device.set_fan_speed(FanSpeed.FAN_SPEED_4) + elif fan_mode == FAN_MEDIUM: + self._device.set_fan_speed(FanSpeed.FAN_SPEED_7) + elif fan_mode == FAN_HIGH: + self._device.set_fan_speed(FanSpeed.FAN_SPEED_10) + elif fan_mode == FAN_AUTO: + self._device.set_fan_speed(FanSpeed.FAN_SPEED_AUTO) + + def set_hvac_mode(self, hvac_mode): + """Set new target hvac mode.""" + _LOGGER.debug("Set %s heat mode %s", self.name, hvac_mode) + if hvac_mode == HVAC_MODE_OFF: + self._device.turn_off() + elif self._device.state.fan_power == FanPower.POWER_OFF.value: + self._device.turn_on() + if hvac_mode == HVAC_MODE_HEAT: + self._device.enable_heat_mode() + elif hvac_mode == HVAC_MODE_COOL: + self._device.disable_heat_mode() + + @property + def min_temp(self): + """Return the minimum temperature.""" + return 1 + + @property + def max_temp(self): + """Return the maximum temperature.""" + return 37 diff --git a/tests/components/dyson/common.py b/tests/components/dyson/common.py index b78e7d58283..f1dabe5203d 100644 --- a/tests/components/dyson/common.py +++ b/tests/components/dyson/common.py @@ -23,3 +23,4 @@ def load_mock_device(device): device.state.oscillation_angle_low = "000" device.state.oscillation_angle_high = "000" device.state.filter_life = "000" + device.state.heat_target = 200 diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 2d769231f1b..cca589875aa 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -1,19 +1,52 @@ """Test the Dyson fan component.""" +import json import unittest -from unittest import mock -from libpurecool.const import FocusMode, HeatMode, HeatState, HeatTarget +from libpurecool.const import ( + FanPower, + FanSpeed, + FanState, + FocusMode, + HeatMode, + HeatState, + HeatTarget, +) +from libpurecool.dyson_pure_hotcool import DysonPureHotCool from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink from libpurecool.dyson_pure_state import DysonPureHotCoolState +from libpurecool.dyson_pure_state_v2 import DysonPureHotCoolV2State from homeassistant.components import dyson as dyson_parent +from homeassistant.components.climate import ( + DOMAIN, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, + ATTR_FAN_MODE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) from homeassistant.components.dyson import climate as dyson -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.setup import async_setup_component from .common import load_mock_device -from tests.async_mock import patch +from tests.async_mock import MagicMock, Mock, patch from tests.common import get_test_home_assistant @@ -22,7 +55,6 @@ class MockDysonState(DysonPureHotCoolState): def __init__(self): """Create new Mock Dyson State.""" - pass def _get_config(): @@ -40,9 +72,22 @@ def _get_config(): } +def _get_dyson_purehotcool_device(): + """Return a valid device as provided by the Dyson web services.""" + device = Mock(spec=DysonPureHotCool) + load_mock_device(device) + device.name = "Living room" + device.state.heat_target = "0000" + device.state.heat_mode = HeatMode.HEAT_OFF.value + device.state.fan_power = FanPower.POWER_OFF.value + device.environmental_state.humidity = 42 + device.environmental_state.temperature = 298 + return device + + def _get_device_with_no_state(): """Return a device with no state.""" - device = mock.Mock(spec=DysonPureHotCoolLink) + device = Mock(spec=DysonPureHotCoolLink) load_mock_device(device) device.state = None device.environmental_state = None @@ -51,14 +96,14 @@ def _get_device_with_no_state(): def _get_device_off(): """Return a device with state off.""" - device = mock.Mock(spec=DysonPureHotCoolLink) + device = Mock(spec=DysonPureHotCoolLink) load_mock_device(device) return device def _get_device_focus(): """Return a device with fan state of focus mode.""" - device = mock.Mock(spec=DysonPureHotCoolLink) + device = Mock(spec=DysonPureHotCoolLink) load_mock_device(device) device.state.focus_mode = FocusMode.FOCUS_ON.value return device @@ -66,7 +111,7 @@ def _get_device_focus(): def _get_device_diffuse(): """Return a device with fan state of diffuse mode.""" - device = mock.Mock(spec=DysonPureHotCoolLink) + device = Mock(spec=DysonPureHotCoolLink) load_mock_device(device) device.state.focus_mode = FocusMode.FOCUS_OFF.value return device @@ -74,7 +119,7 @@ def _get_device_diffuse(): def _get_device_cool(): """Return a device with state of cooling.""" - device = mock.Mock(spec=DysonPureHotCoolLink) + device = Mock(spec=DysonPureHotCoolLink) load_mock_device(device) device.state.focus_mode = FocusMode.FOCUS_OFF.value device.state.heat_target = HeatTarget.celsius(12) @@ -85,7 +130,7 @@ def _get_device_cool(): def _get_device_heat_off(): """Return a device with state of heat reached target.""" - device = mock.Mock(spec=DysonPureHotCoolLink) + device = Mock(spec=DysonPureHotCoolLink) load_mock_device(device) device.state.heat_mode = HeatMode.HEAT_ON.value device.state.heat_state = HeatState.HEAT_STATE_OFF.value @@ -94,7 +139,7 @@ def _get_device_heat_off(): def _get_device_heat_on(): """Return a device with state of heating.""" - device = mock.Mock(spec=DysonPureHotCoolLink) + device = Mock(spec=DysonPureHotCoolLink) load_mock_device(device) device.serial = "YY-YYYYY-YY" device.state.heat_target = HeatTarget.celsius(23) @@ -120,7 +165,7 @@ class DysonTest(unittest.TestCase): def test_setup_component_without_devices(self): """Test setup component with no devices.""" self.hass.data[dyson.DYSON_DEVICES] = [] - add_devices = mock.MagicMock() + add_devices = MagicMock() dyson.setup_platform(self.hass, None, add_devices) add_devices.assert_not_called() @@ -132,18 +177,10 @@ class DysonTest(unittest.TestCase): _get_device_heat_on(), ] self.hass.data[dyson.DYSON_DEVICES] = devices - add_devices = mock.MagicMock() + add_devices = MagicMock() dyson.setup_platform(self.hass, None, add_devices, discovery_info={}) assert add_devices.called - def test_setup_component_with_invalid_devices(self): - """Test setup component with invalid devices.""" - devices = [None, "foo_bar"] - self.hass.data[dyson.DYSON_DEVICES] = devices - add_devices = mock.MagicMock() - dyson.setup_platform(self.hass, None, add_devices, discovery_info={}) - add_devices.assert_called_with([]) - def test_setup_component(self): """Test setup component with devices.""" device_fan = _get_device_heat_on() @@ -160,7 +197,7 @@ class DysonTest(unittest.TestCase): """Test set climate temperature.""" device = _get_device_heat_on() device.temp_unit = TEMP_CELSIUS - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert not entity.should_poll # Without target temp. @@ -195,8 +232,8 @@ class DysonTest(unittest.TestCase): """Test set climate temperature when heating is off.""" device = _get_device_cool() device.temp_unit = TEMP_CELSIUS - entity = dyson.DysonPureHotCoolLinkDevice(device) - entity.schedule_update_ha_state = mock.Mock() + entity = dyson.DysonPureHotCoolLinkEntity(device) + entity.schedule_update_ha_state = Mock() kwargs = {ATTR_TEMPERATURE: 23} entity.set_temperature(**kwargs) @@ -208,7 +245,7 @@ class DysonTest(unittest.TestCase): def test_dyson_set_fan_mode(self): """Test set fan mode.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert not entity.should_poll entity.set_fan_mode(dyson.FAN_FOCUS) @@ -222,7 +259,7 @@ class DysonTest(unittest.TestCase): def test_dyson_fan_modes(self): """Test get fan list.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert len(entity.fan_modes) == 2 assert dyson.FAN_FOCUS in entity.fan_modes assert dyson.FAN_DIFFUSE in entity.fan_modes @@ -230,19 +267,19 @@ class DysonTest(unittest.TestCase): def test_dyson_fan_mode_focus(self): """Test fan focus mode.""" device = _get_device_focus() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.fan_mode == dyson.FAN_FOCUS def test_dyson_fan_mode_diffuse(self): """Test fan diffuse mode.""" device = _get_device_diffuse() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.fan_mode == dyson.FAN_DIFFUSE def test_dyson_set_hvac_mode(self): """Test set operation mode.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert not entity.should_poll entity.set_hvac_mode(dyson.HVAC_MODE_HEAT) @@ -256,7 +293,7 @@ class DysonTest(unittest.TestCase): def test_dyson_operation_list(self): """Test get operation list.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert len(entity.hvac_modes) == 2 assert dyson.HVAC_MODE_HEAT in entity.hvac_modes assert dyson.HVAC_MODE_COOL in entity.hvac_modes @@ -264,7 +301,7 @@ class DysonTest(unittest.TestCase): def test_dyson_heat_off(self): """Test turn off heat.""" device = _get_device_heat_off() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) entity.set_hvac_mode(dyson.HVAC_MODE_COOL) set_config = device.set_configuration set_config.assert_called_with(heat_mode=HeatMode.HEAT_OFF) @@ -272,7 +309,7 @@ class DysonTest(unittest.TestCase): def test_dyson_heat_on(self): """Test turn on heat.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) entity.set_hvac_mode(dyson.HVAC_MODE_HEAT) set_config = device.set_configuration set_config.assert_called_with(heat_mode=HeatMode.HEAT_ON) @@ -280,34 +317,34 @@ class DysonTest(unittest.TestCase): def test_dyson_heat_value_on(self): """Test get heat value on.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.hvac_mode == dyson.HVAC_MODE_HEAT def test_dyson_heat_value_off(self): """Test get heat value off.""" device = _get_device_cool() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.hvac_mode == dyson.HVAC_MODE_COOL def test_dyson_heat_value_idle(self): """Test get heat value idle.""" device = _get_device_heat_off() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.hvac_mode == dyson.HVAC_MODE_HEAT assert entity.hvac_action == dyson.CURRENT_HVAC_IDLE def test_on_message(self): """Test when message is received.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) - entity.schedule_update_ha_state = mock.Mock() + entity = dyson.DysonPureHotCoolLinkEntity(device) + entity.schedule_update_ha_state = Mock() entity.on_message(MockDysonState()) entity.schedule_update_ha_state.assert_called_with() def test_general_properties(self): """Test properties of entity.""" device = _get_device_with_no_state() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.should_poll is False assert entity.supported_features == dyson.SUPPORT_FLAGS assert entity.temperature_unit == TEMP_CELSIUS @@ -315,41 +352,41 @@ class DysonTest(unittest.TestCase): def test_property_current_humidity(self): """Test properties of current humidity.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.current_humidity == 53 def test_property_current_humidity_with_invalid_env_state(self): """Test properties of current humidity with invalid env state.""" device = _get_device_off() device.environmental_state.humidity = 0 - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.current_humidity is None def test_property_current_humidity_without_env_state(self): """Test properties of current humidity without env state.""" device = _get_device_with_no_state() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.current_humidity is None def test_property_current_temperature(self): """Test properties of current temperature.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) # Result should be in celsius, hence then subtraction of 273. assert entity.current_temperature == 289 - 273 def test_property_target_temperature(self): """Test properties of target temperature.""" device = _get_device_heat_on() - entity = dyson.DysonPureHotCoolLinkDevice(device) + entity = dyson.DysonPureHotCoolLinkEntity(device) assert entity.target_temperature == 23 @patch( - "libpurecool.dyson.DysonAccount.devices", + "homeassistant.components.dyson.DysonAccount.devices", return_value=[_get_device_heat_on(), _get_device_cool()], ) -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) async def test_setup_component_with_parent_discovery( mocked_login, mocked_devices, hass ): @@ -357,4 +394,328 @@ async def test_setup_component_with_parent_discovery( await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) await hass.async_block_till_done() - assert len(hass.data[dyson.DYSON_DEVICES]) == 2 + entity_ids = hass.states.async_entity_ids("climate") + assert len(entity_ids) == 2 + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_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() + + entity_ids = hass.states.async_entity_ids("climate") + assert len(entity_ids) == 1 + state = hass.states.get(entity_ids[0]) + assert state.name == "Living room" + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_device_off()], +) +async def test_purehotcoollink_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() + + entity_ids = hass.states.async_entity_ids("climate") + assert len(entity_ids) == 1 + state = hass.states.get(entity_ids[0]) + assert state.name == "Temp Name" + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_update_state(devices, login, hass): + """Test state update.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + event = { + "msg": "CURRENT-STATE", + "product-state": { + "fpwr": "ON", + "fdir": "OFF", + "auto": "OFF", + "oscs": "ON", + "oson": "ON", + "nmod": "OFF", + "rhtm": "ON", + "fnst": "FAN", + "ercd": "11E1", + "wacd": "NONE", + "nmdv": "0004", + "fnsp": "0002", + "bril": "0002", + "corf": "ON", + "cflr": "0085", + "hflr": "0095", + "sltm": "OFF", + "osal": "0045", + "osau": "0095", + "ancp": "CUST", + "tilt": "OK", + "hsta": "HEAT", + "hmax": "2986", + "hmod": "HEAT", + }, + } + device.state = DysonPureHotCoolV2State(json.dumps(event)) + + for call in device.add_message_listener.call_args_list: + callback = call[0][0] + if type(callback.__self__) == dyson.DysonPureHotCoolEntity: + callback(device.state) + + await hass.async_block_till_done() + state = hass.states.get("climate.living_room") + attributes = state.attributes + + assert attributes[ATTR_TEMPERATURE] == 25 + assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_empty_env_attributes(devices, login, hass): + """Test empty environmental state update.""" + device = devices.return_value[0] + device.environmental_state.temperature = None + device.environmental_state.humidity = None + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + + state = hass.states.get("climate.living_room") + attributes = state.attributes + + assert ATTR_CURRENT_HUMIDITY not in attributes + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_fan_state_off(devices, login, hass): + """Test device fan state off.""" + device = devices.return_value[0] + device.state.fan_state = FanState.FAN_OFF.value + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + + state = hass.states.get("climate.living_room") + attributes = state.attributes + + assert attributes[ATTR_FAN_MODE] == FAN_OFF + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_hvac_action_cool(devices, login, hass): + """Test device HVAC action cool.""" + device = devices.return_value[0] + device.state.fan_power = FanPower.POWER_ON.value + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + + state = hass.states.get("climate.living_room") + attributes = state.attributes + + assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_hvac_action_idle(devices, login, hass): + """Test device HVAC action idle.""" + device = devices.return_value[0] + device.state.fan_power = FanPower.POWER_ON.value + device.state.heat_mode = HeatMode.HEAT_ON.value + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + + state = hass.states.get("climate.living_room") + attributes = state.attributes + + assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_set_temperature(devices, login, hass): + """Test set temperature.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + state = hass.states.get("climate.living_room") + attributes = state.attributes + min_temp = attributes["min_temp"] + max_temp = attributes["max_temp"] + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.bed_room", ATTR_TEMPERATURE: 23}, + True, + ) + device.set_heat_target.assert_not_called() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_TEMPERATURE: 23}, + True, + ) + assert device.set_heat_target.call_count == 1 + device.set_heat_target.assert_called_with("2960") + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_TEMPERATURE: min_temp - 1}, + True, + ) + assert device.set_heat_target.call_count == 2 + device.set_heat_target.assert_called_with(HeatTarget.celsius(min_temp)) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_TEMPERATURE: max_temp + 1}, + True, + ) + assert device.set_heat_target.call_count == 3 + device.set_heat_target.assert_called_with(HeatTarget.celsius(max_temp)) + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_set_fan_mode(devices, login, hass): + """Test set fan mode.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.bed_room", ATTR_FAN_MODE: FAN_OFF}, + True, + ) + device.turn_off.assert_not_called() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_OFF}, + True, + ) + assert device.turn_off.call_count == 1 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_LOW}, + True, + ) + assert device.set_fan_speed.call_count == 1 + device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_4) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_MEDIUM}, + True, + ) + assert device.set_fan_speed.call_count == 2 + device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_7) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_HIGH}, + True, + ) + assert device.set_fan_speed.call_count == 3 + device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_10) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_AUTO}, + True, + ) + assert device.set_fan_speed.call_count == 4 + device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_AUTO) + + +@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) +@patch( + "homeassistant.components.dyson.DysonAccount.devices", + return_value=[_get_dyson_purehotcool_device()], +) +async def test_purehotcool_set_hvac_mode(devices, login, hass): + """Test set HVAC mode.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.bed_room", ATTR_HVAC_MODE: HVAC_MODE_OFF}, + True, + ) + device.turn_off.assert_not_called() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVAC_MODE_OFF}, + True, + ) + assert device.turn_off.call_count == 1 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + True, + ) + assert device.turn_on.call_count == 1 + assert device.enable_heat_mode.call_count == 1 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVAC_MODE_COOL}, + True, + ) + assert device.turn_on.call_count == 2 + assert device.disable_heat_mode.call_count == 1