diff --git a/.coveragerc b/.coveragerc index 87886b84120db2..93b1675a963e77 100644 --- a/.coveragerc +++ b/.coveragerc @@ -479,8 +479,6 @@ omit = homeassistant/components/homematic/sensor.py homeassistant/components/homematic/switch.py homeassistant/components/homeworks/* - homeassistant/components/honeywell/__init__.py - homeassistant/components/honeywell/climate.py homeassistant/components/horizon/media_player.py homeassistant/components/hp_ilo/sensor.py homeassistant/components/huawei_lte/__init__.py diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index e9dae1e207455c..e442f71ad8f8c0 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -212,9 +212,9 @@ def current_humidity(self) -> int | None: return self._device.current_humidity @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode.""" - return HW_MODE_TO_HVAC_MODE[self._device.system_mode] + return HW_MODE_TO_HVAC_MODE.get(self._device.system_mode) @property def hvac_action(self) -> HVACAction | None: @@ -341,12 +341,8 @@ async def _turn_away_mode_on(self) -> None: it doesn't get overwritten when away mode is switched on. """ self._away = True - try: - # Get current mode - mode = self._device.system_mode - except aiosomecomfort.SomeComfortError: - _LOGGER.error("Can not get system mode") - return + # Get current mode + mode = self._device.system_mode try: # Set permanent hold # and Set temperature @@ -365,12 +361,8 @@ async def _turn_away_mode_on(self) -> None: async def _turn_hold_mode_on(self) -> None: """Turn permanent hold on.""" - try: - # Get current mode - mode = self._device.system_mode - except aiosomecomfort.SomeComfortError: - _LOGGER.error("Can not get system mode") - return + # Get current mode + mode = self._device.system_mode # Check that we got a valid mode back if mode in HW_MODE_TO_HVAC_MODE: try: diff --git a/tests/components/honeywell/__init__.py b/tests/components/honeywell/__init__.py index 7c6b4ca78c60dc..6299097b104c66 100644 --- a/tests/components/honeywell/__init__.py +++ b/tests/components/honeywell/__init__.py @@ -1 +1,25 @@ """Tests for honeywell component.""" +from unittest.mock import MagicMock + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def init_integration( + hass: HomeAssistant, entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the Honeywell integration in Home Assistant.""" + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +def reset_mock(device: MagicMock) -> None: + """Reset the mocks for test.""" + device.set_setpoint_cool.reset_mock() + device.set_setpoint_heat.reset_mock() + device.set_hold_heat.reset_mock() + device.set_hold_cool.reset_mock() diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index 95e1758ec221a7..bedd42909443f1 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -5,25 +5,53 @@ import aiosomecomfort import pytest -from homeassistant.components.honeywell.const import DOMAIN +from homeassistant.components.honeywell.const import ( + CONF_COOL_AWAY_TEMPERATURE, + CONF_HEAT_AWAY_TEMPERATURE, + DOMAIN, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry +HEATUPPERSETPOINTLIMIT = 35 +HEATLOWERSETPOINTLIMIT = 20 +COOLUPPERSETPOINTLIMIT = 20 +COOLLOWERSETPOINTLIMIT = 10 +NEXTCOOLPERIOD = 10 +NEXTHEATPERIOD = 10 +OUTDOORTEMP = 5 +OUTDOORHUMIDITY = 25 +CURRENTTEMPERATURE = 20 +CURRENTHUMIDITY = 50 +HEATAWAY = 10 +COOLAWAY = 20 +SETPOINTCOOL = 26 +SETPOINTHEAT = 18 + @pytest.fixture def config_data(): """Provide configuration data for tests.""" - return {CONF_USERNAME: "fake", CONF_PASSWORD: "user"} + return { + CONF_USERNAME: "fake", + CONF_PASSWORD: "user", + } + + +@pytest.fixture +def config_options(): + """Provide configuratio options for test.""" + return {CONF_COOL_AWAY_TEMPERATURE: 12, CONF_HEAT_AWAY_TEMPERATURE: 22} @pytest.fixture -def config_entry(config_data): +def config_entry(config_data, config_options): """Create a mock config entry.""" return MockConfigEntry( domain=DOMAIN, data=config_data, - options={}, + options=config_options, ) @@ -33,15 +61,53 @@ def device(): mock_device = create_autospec(aiosomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { - "canControlHumidification": False, - "hasFan": False, + "canControlHumidification": True, + "hasFan": True, } mock_device.system_mode = "off" mock_device.name = "device1" - mock_device.current_temperature = 20 + mock_device.current_temperature = CURRENTTEMPERATURE mock_device.mac_address = "macaddress1" mock_device.outdoor_temperature = None mock_device.outdoor_humidity = None + mock_device.is_alive = True + mock_device.fan_running = False + mock_device.fan_mode = "auto" + mock_device.setpoint_cool = SETPOINTCOOL + mock_device.setpoint_heat = SETPOINTHEAT + mock_device.hold_heat = False + mock_device.hold_cool = False + mock_device.current_humidity = CURRENTHUMIDITY + mock_device.equipment_status = "off" + mock_device.equipment_output_status = "off" + mock_device.raw_ui_data = { + "SwitchOffAllowed": True, + "SwitchAutoAllowed": True, + "SwitchCoolAllowed": True, + "SwitchHeatAllowed": True, + "SwitchEmergencyHeatAllowed": True, + "HeatUpperSetptLimit": HEATUPPERSETPOINTLIMIT, + "HeatLowerSetptLimit": HEATLOWERSETPOINTLIMIT, + "CoolUpperSetptLimit": COOLUPPERSETPOINTLIMIT, + "CoolLowerSetptLimit": COOLLOWERSETPOINTLIMIT, + "HeatNextPeriod": NEXTHEATPERIOD, + "CoolNextPeriod": NEXTCOOLPERIOD, + } + mock_device.raw_fan_data = { + "fanModeOnAllowed": True, + "fanModeAutoAllowed": True, + "fanModeCirculateAllowed": True, + } + mock_device.set_setpoint_cool = AsyncMock() + mock_device.set_setpoint_heat = AsyncMock() + mock_device.set_system_mode = AsyncMock() + mock_device.set_fan_mode = AsyncMock() + mock_device.set_hold_heat = AsyncMock() + mock_device.set_hold_cool = AsyncMock() + mock_device.refresh = AsyncMock() + mock_device.heat_away_temp = HEATAWAY + mock_device.cool_away_temp = COOLAWAY + return mock_device @@ -56,11 +122,11 @@ def device_with_outdoor_sensor(): } mock_device.system_mode = "off" mock_device.name = "device1" - mock_device.current_temperature = 20 + mock_device.current_temperature = CURRENTTEMPERATURE mock_device.mac_address = "macaddress1" mock_device.temperature_unit = "C" - mock_device.outdoor_temperature = 5 - mock_device.outdoor_humidity = 25 + mock_device.outdoor_temperature = OUTDOORTEMP + mock_device.outdoor_humidity = OUTDOORHUMIDITY return mock_device @@ -75,7 +141,7 @@ def another_device(): } mock_device.system_mode = "off" mock_device.name = "device2" - mock_device.current_temperature = 20 + mock_device.current_temperature = CURRENTTEMPERATURE mock_device.mac_address = "macaddress1" mock_device.outdoor_temperature = None mock_device.outdoor_humidity = None diff --git a/tests/components/honeywell/snapshots/test_climate.ambr b/tests/components/honeywell/snapshots/test_climate.ambr new file mode 100644 index 00000000000000..4f7d8fe1308381 --- /dev/null +++ b/tests/components/honeywell/snapshots/test_climate.ambr @@ -0,0 +1,38 @@ +# serializer version: 1 +# name: test_static_attributes + ReadOnlyDict({ + 'aux_heat': 'off', + 'current_humidity': 50, + 'current_temperature': -6.7, + 'fan_action': 'idle', + 'fan_mode': 'auto', + 'fan_modes': list([ + 'on', + 'auto', + 'diffuse', + ]), + 'friendly_name': 'device1', + 'humidity': None, + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_humidity': 99, + 'max_temp': 1.7, + 'min_humidity': 30, + 'min_temp': -13.9, + 'permanent_hold': False, + 'preset_mode': None, + 'preset_modes': list([ + 'none', + 'away', + 'Hold', + ]), + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': None, + }) +# --- diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py new file mode 100644 index 00000000000000..0f65a786b447f8 --- /dev/null +++ b/tests/components/honeywell/test_climate.py @@ -0,0 +1,1061 @@ +"""Test the Whirlpool Sixth Sense climate domain.""" +import datetime +from unittest.mock import MagicMock, patch + +import aiosomecomfort +import pytest +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.components.climate import ( + ATTR_AUX_HEAT, + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE_DOMAIN, + FAN_AUTO, + FAN_DIFFUSE, + FAN_ON, + PRESET_AWAY, + PRESET_NONE, + SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.components.honeywell.climate import SCAN_INTERVAL +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import init_integration, reset_mock + +from tests.common import async_fire_time_changed + +FAN_ACTION = "fan_action" +PRESET_HOLD = "Hold" + + +async def test_no_thermostats( + hass: HomeAssistant, device: MagicMock, config_entry: MagicMock +) -> None: + """Test the setup of the climate entities when there are no appliances available.""" + device._data = {} + await init_integration(hass, config_entry) + assert len(hass.states.async_all()) == 0 + + +async def test_static_attributes( + hass: HomeAssistant, + device: MagicMock, + config_entry: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test static climate attributes.""" + await init_integration(hass, config_entry) + + entity_id = f"climate.{device.name}" + entry = er.async_get(hass).async_get(entity_id) + assert entry + + state = hass.states.get(entity_id) + assert state.state == HVACMode.OFF + + attributes = state.attributes + + assert attributes == snapshot(exclude=props("dr_phase")) + + +async def test_dynamic_attributes( + hass: HomeAssistant, device: MagicMock, config_entry: MagicMock +) -> None: + """Test dynamic attributes.""" + + await init_integration(hass, config_entry) + + entity_id = f"climate.{device.name}" + state = hass.states.get(entity_id) + assert state.state == HVACMode.OFF + attributes = state.attributes + assert attributes["current_temperature"] == -6.7 + assert attributes["current_humidity"] == 50 + + device.system_mode = "cool" + device.current_temperature = 21 + device.current_humidity = 55 + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == HVACMode.COOL + attributes = state.attributes + assert attributes["current_temperature"] == -6.1 + assert attributes["current_humidity"] == 55 + + device.system_mode = "heat" + device.current_temperature = 61 + device.current_humidity = 50 + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == HVACMode.HEAT + attributes = state.attributes + assert attributes["current_temperature"] == 16.1 + assert attributes["current_humidity"] == 50 + + device.system_mode = "auto" + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == HVACMode.HEAT_COOL + attributes = state.attributes + assert attributes["current_temperature"] == 16.1 + assert attributes["current_humidity"] == 50 + + +async def test_mode_service_calls( + hass: HomeAssistant, + device: MagicMock, + config_entry: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test controlling the entity mode through service calls.""" + await init_integration(hass, config_entry) + entity_id = f"climate.{device.name}" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("off") + + device.set_system_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("auto") + + device.set_system_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.COOL}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("cool") + + device.set_system_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("heat") + + device.set_system_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("auto") + + device.set_system_mode.reset_mock() + + +async def test_auxheat_service_calls( + hass: HomeAssistant, device: MagicMock, config_entry: MagicMock +) -> None: + """Test controlling the auxheat through service calls.""" + await init_integration(hass, config_entry) + entity_id = f"climate.{device.name}" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: entity_id, ATTR_AUX_HEAT: True}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("emheat") + + device.set_system_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: entity_id, ATTR_AUX_HEAT: False}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("heat") + + +async def test_fan_modes_service_calls( + hass: HomeAssistant, device: MagicMock, config_entry: MagicMock +) -> None: + """Test controlling the fan modes through service calls.""" + await init_integration(hass, config_entry) + entity_id = f"climate.{device.name}" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_AUTO}, + blocking=True, + ) + + device.set_fan_mode.assert_called_once_with("auto") + + device.set_fan_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_ON}, + blocking=True, + ) + + device.set_fan_mode.assert_called_once_with("on") + + device.set_fan_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_DIFFUSE}, + blocking=True, + ) + + device.set_fan_mode.assert_called_once_with("circulate") + + +async def test_service_calls_off_mode( + hass: HomeAssistant, + device: MagicMock, + config_entry: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test controlling the entity through service calls.""" + + device.system_mode = "off" + + await init_integration(hass, config_entry) + entity_id = f"climate.{device.name}" + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_cool.assert_called_with(95) + device.set_setpoint_heat.assert_called_with(77) + + device.set_setpoint_heat.reset_mock() + device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_cool.assert_called_with(95) + device.set_setpoint_heat.assert_called_with(77) + assert "Invalid temperature" in caplog.messages[-1] + + reset_mock(device) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + device.set_setpoint_heat.assert_not_called() + device.set_setpoint_cool.assert_not_called() + + reset_mock(device) + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_cool.assert_not_called() + device.set_hold_heat.assert_not_called() + + reset_mock(device) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + + device.set_hold_cool.assert_not_called() + device.set_setpoint_cool.assert_not_called() + device.set_hold_heat.assert_not_called() + device.set_setpoint_heat.assert_not_called() + + device.set_hold_heat.reset_mock() + device.set_hold_cool.reset_mock() + + device.set_setpoint_cool.reset_mock() + device.set_setpoint_heat.reset_mock() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(False) + device.set_hold_cool.assert_called_once_with(False) + + reset_mock(device) + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_heat.assert_not_called() + device.set_hold_cool.assert_not_called() + + reset_mock(device) + device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_heat.assert_not_called() + device.set_hold_cool.assert_not_called() + + +async def test_service_calls_cool_mode( + hass: HomeAssistant, + device: MagicMock, + config_entry: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test controlling the entity through service calls.""" + + device.system_mode = "cool" + + await init_integration(hass, config_entry) + entity_id = f"climate.{device.name}" + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + device.set_hold_cool.assert_called_once_with(datetime.time(2, 30), 59) + device.set_hold_cool.reset_mock() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_cool.assert_called_with(95) + device.set_setpoint_heat.assert_called_with(77) + + device.set_setpoint_cool.reset_mock() + device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_cool.assert_called_with(95) + device.set_setpoint_heat.assert_called_with(77) + assert "Invalid temperature" in caplog.messages[-1] + + reset_mock(device) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True, 12) + device.set_hold_heat.assert_not_called() + device.set_setpoint_heat.assert_not_called() + + reset_mock(device) + + device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True, 12) + device.set_hold_heat.assert_not_called() + device.set_setpoint_heat.assert_not_called() + assert "Temperature out of range" in caplog.messages[-1] + + reset_mock(device) + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True) + device.set_hold_heat.assert_not_called() + + device.hold_heat = True + device.hold_cool = True + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: "20"}, + blocking=True, + ) + + device.set_setpoint_cool.assert_called_once() + + reset_mock(device) + device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + device.set_hold_cool.assert_called_once_with(True) + device.set_hold_heat.assert_not_called() + assert "Couldn't set permanent hold" in caplog.messages[-1] + + reset_mock(device) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + device.set_hold_heat.assert_not_called() + device.set_hold_cool.assert_called_once_with(False) + + reset_mock(device) + + device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + device.set_hold_heat.assert_not_called() + device.set_hold_cool.assert_called_once_with(False) + assert "Can not stop hold mode" in caplog.messages[-1] + + reset_mock(device) + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True) + device.set_hold_heat.assert_not_called() + + reset_mock(device) + device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True) + device.set_hold_heat.assert_not_called() + assert "Couldn't set permanent hold" in caplog.messages[-1] + + reset_mock(device) + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + device.system_mode = "Junk" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_cool.assert_not_called() + device.set_hold_heat.assert_not_called() + assert "Invalid system mode returned" in caplog.messages[-2] + + +async def test_service_calls_heat_mode( + hass: HomeAssistant, + device: MagicMock, + config_entry: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test controlling the entity through service calls.""" + + device.system_mode = "heat" + + await init_integration(hass, config_entry) + entity_id = f"climate.{device.name}" + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59) + device.set_hold_heat.reset_mock() + + device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59) + device.set_hold_heat.reset_mock() + assert "Invalid temperature" in caplog.messages[-1] + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_cool.assert_called_with(95) + device.set_setpoint_heat.assert_called_with(77) + + device.set_setpoint_heat.reset_mock() + device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_cool.assert_called_with(95) + device.set_setpoint_heat.assert_called_with(77) + assert "Invalid temperature" in caplog.messages[-1] + + reset_mock(device) + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(True) + device.set_hold_cool.assert_not_called() + + device.hold_heat = True + device.hold_cool = True + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: "20"}, + blocking=True, + ) + + device.set_setpoint_heat.assert_called_once() + + reset_mock(device) + + device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + device.set_hold_heat.assert_called_once_with(True) + device.set_hold_cool.assert_not_called() + assert "Couldn't set permanent hold" in caplog.messages[-1] + + reset_mock(device) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(True, 22) + device.set_hold_cool.assert_not_called() + device.set_setpoint_cool.assert_not_called() + + reset_mock(device) + + device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(True, 22) + device.set_hold_cool.assert_not_called() + device.set_setpoint_cool.assert_not_called() + assert "Temperature out of range" in caplog.messages[-1] + + reset_mock(device) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(False) + device.set_hold_cool.assert_called_once_with(False) + + device.set_hold_heat.reset_mock() + device.set_hold_cool.reset_mock() + device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(False) + assert "Can not stop hold mode" in caplog.messages[-1] + + reset_mock(device) + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(True) + device.set_hold_cool.assert_not_called() + + reset_mock(device) + device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(True) + device.set_hold_cool.assert_not_called() + + reset_mock(device) + + +async def test_service_calls_auto_mode( + hass: HomeAssistant, + device: MagicMock, + config_entry: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test controlling the entity through service calls.""" + + device.system_mode = "auto" + + await init_integration(hass, config_entry) + entity_id = f"climate.{device.name}" + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + device.set_setpoint_cool.assert_not_called() + device.set_setpoint_heat.assert_not_called() + + reset_mock(device) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_cool.assert_called_once_with(95) + device.set_setpoint_heat.assert_called_once_with(77) + + reset_mock(device) + + device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError + device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + device.set_setpoint_heat.assert_not_called() + assert "Invalid temperature" in caplog.messages[-1] + + reset_mock(device) + + device.set_setpoint_heat.side_effect = aiosomecomfort.SomeComfortError + device.set_setpoint_cool.side_effect = aiosomecomfort.SomeComfortError + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_heat.assert_not_called() + assert "Invalid temperature" in caplog.messages[-1] + + reset_mock(device) + + device.set_hold_heat.side_effect = None + device.set_hold_cool.side_effect = None + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True) + device.set_hold_heat.assert_called_once_with(True) + + reset_mock(device) + + device.set_hold_heat.side_effect = aiosomecomfort.SomeComfortError + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + device.set_hold_cool.assert_called_once_with(True) + device.set_hold_heat.assert_called_once_with(True) + assert "Couldn't set permanent hold" in caplog.messages[-1] + + reset_mock(device) + device.set_setpoint_heat.side_effect = None + device.set_setpoint_cool.side_effect = None + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True, 12) + device.set_hold_heat.assert_called_once_with(True, 22) + + reset_mock(device) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + device.set_hold_heat.assert_called_once_with(False) + device.set_hold_cool.assert_called_once_with(False) + + reset_mock(device) + device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + device.set_hold_heat.assert_not_called() + device.set_hold_cool.assert_called_once_with(False) + assert "Can not stop hold mode" in caplog.messages[-1] + + reset_mock(device) + + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True) + device.set_hold_heat.assert_not_called() + + reset_mock(device) + + device.set_hold_cool.side_effect = aiosomecomfort.SomeComfortError + device.raw_ui_data["StatusHeat"] = 2 + device.raw_ui_data["StatusCool"] = 2 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_HOLD}, + blocking=True, + ) + + device.set_hold_cool.assert_called_once_with(True) + device.set_hold_heat.assert_not_called() + assert "Couldn't set permanent hold" in caplog.messages[-1] + + +async def test_async_update_errors( + hass: HomeAssistant, + device: MagicMock, + config_entry: MagicMock, + client: MagicMock, +) -> None: + """Test update with errors.""" + + await init_integration(hass, config_entry) + + device.refresh.side_effect = aiosomecomfort.SomeComfortError + client.login.side_effect = aiosomecomfort.SomeComfortError + entity_id = f"climate.{device.name}" + state = hass.states.get(entity_id) + assert state.state == "off" + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == "unavailable" + + reset_mock(device) + device.refresh.side_effect = None + client.login.side_effect = None + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "off" + + # "reload integration" test + device.refresh.side_effect = aiosomecomfort.SomeComfortError + client.login.side_effect = aiosomecomfort.AuthError + with patch("homeassistant.config_entries.ConfigEntries.async_reload") as reload: + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + entity_id = f"climate.{device.name}" + state = hass.states.get(entity_id) + assert state.state == "unavailable" + assert reload.called_once() + + +async def test_aux_heat_off_service_call( + hass: HomeAssistant, device: MagicMock, config_entry: MagicMock +) -> None: + """Test aux heat off turns of system when no heat configured.""" + device.raw_ui_data["SwitchHeatAllowed"] = False + device.raw_ui_data["SwitchAutoAllowed"] = False + device.raw_ui_data["SwitchEmergencyHeatAllowed"] = True + + await init_integration(hass, config_entry) + + entity_id = f"climate.{device.name}" + entry = er.async_get(hass).async_get(entity_id) + assert entry + + state = hass.states.get(entity_id) + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == HVACMode.OFF + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_AUX_HEAT, + {ATTR_ENTITY_ID: entity_id, ATTR_AUX_HEAT: False}, + blocking=True, + ) + device.set_system_mode.assert_called_once_with("off") diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 29c253d6a5e766..36c94c83f318ea 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -1,5 +1,5 @@ """Test honeywell setup process.""" -from unittest.mock import create_autospec, patch +from unittest.mock import MagicMock, create_autospec, patch import aiosomecomfort import pytest @@ -13,6 +13,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from . import init_integration + from tests.common import MockConfigEntry MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} @@ -28,7 +30,6 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) - assert hass.states.async_entity_ids_count() == 1 -@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) async def test_setup_multiple_thermostats( hass: HomeAssistant, config_entry: MockConfigEntry, location, another_device ) -> None: @@ -41,7 +42,6 @@ async def test_setup_multiple_thermostats( assert hass.states.async_entity_ids_count() == 2 -@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) async def test_setup_multiple_thermostats_with_same_deviceid( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -82,3 +82,30 @@ async def test_away_temps_migration(hass: HomeAssistant) -> None: CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, } + + +async def test_login_error( + hass: HomeAssistant, client: MagicMock, config_entry: MagicMock +) -> None: + """Test login errors from API.""" + client.login.side_effect = aiosomecomfort.AuthError + await init_integration(hass, config_entry) + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_connection_error( + hass: HomeAssistant, client: MagicMock, config_entry: MagicMock +) -> None: + """Test Connection errors from API.""" + client.login.side_effect = aiosomecomfort.ConnectionError + await init_integration(hass, config_entry) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_no_devices( + hass: HomeAssistant, client: MagicMock, config_entry: MagicMock +) -> None: + """Test no devices from API.""" + client.locations_by_id = {} + await init_integration(hass, config_entry) + assert config_entry.state is ConfigEntryState.SETUP_ERROR