From 4a089b5c2810e3d18d49ea97cbe149393ac15b90 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 22 Nov 2022 17:04:55 +0200 Subject: [PATCH] Add tests coverage for Shelly climate platform (#82529) --- .coveragerc | 1 - homeassistant/components/shelly/climate.py | 5 - tests/components/shelly/conftest.py | 5 +- tests/components/shelly/test_climate.py | 374 +++++++++++++++++++++ 4 files changed, 378 insertions(+), 7 deletions(-) create mode 100644 tests/components/shelly/test_climate.py diff --git a/.coveragerc b/.coveragerc index 98a9ee9c6ff..f3931e47155 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1110,7 +1110,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/shelly/climate.py homeassistant/components/shelly/coordinator.py homeassistant/components/shelly/utils.py homeassistant/components/shiftr/* diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index c55c0261839..cc831c10ee0 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -27,7 +27,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import LOGGER, SHTRV_01_TEMPERATURE_SETTINGS from .coordinator import ShellyBlockCoordinator, get_entry_data -from .utils import get_device_entry_gen async def async_setup_entry( @@ -36,10 +35,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up climate device.""" - - if get_device_entry_gen(config_entry) == 2: - return - coordinator = get_entry_data(hass)[config_entry.entry_id].block assert coordinator if coordinator.device.initialized: diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 26931ea804e..bfa8e903155 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -30,7 +30,7 @@ MOCK_SETTINGS = { "relays": [{"btn_type": "momentary"}, {"btn_type": "toggle"}], "rollers": [{"positioning": True}], "external_power": 0, - "thermostats": [{"schedule_profile_names": {}}], + "thermostats": [{"schedule_profile_names": ["Profile1", "Profile2"]}], } @@ -100,9 +100,11 @@ MOCK_BLOCKS = [ ), Mock( sensor_ids={"motion": 0, "temp": 22.1, "gas": "mild"}, + channel="0", motion=0, temp=22.1, gas="mild", + targetTemp=4, description="sensor_0", type="sensor", ), @@ -111,6 +113,7 @@ MOCK_BLOCKS = [ channel="0", battery=98, cfgChanged=0, + mode=0, valvePos=50, description="device_0", type="device", diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py new file mode 100644 index 00000000000..56effa156e6 --- /dev/null +++ b/tests/components/shelly/test_climate.py @@ -0,0 +1,374 @@ +"""Tests for Shelly climate platform.""" +from unittest.mock import AsyncMock, PropertyMock + +from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError +import pytest + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE_DOMAIN, + PRESET_NONE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.components.shelly.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE +from homeassistant.core import State +from homeassistant.exceptions import HomeAssistantError + +from . import init_integration, register_device, register_entity + +from tests.common import mock_restore_cache + +SENSOR_BLOCK_ID = 3 +DEVICE_BLOCK_ID = 4 +ENTITY_ID = f"{CLIMATE_DOMAIN}.test_name" + + +async def test_climate_hvac_mode(hass, mock_block_device, monkeypatch): + """Test climate hvac mode service.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + # Test initial hvac mode - off + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.OFF + + # Test set hvac mode heat + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": 20.0} + ) + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 20.0) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.HEAT + + # Test set hvac mode off + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF}, + blocking=True, + ) + + mock_block_device.http_request.assert_called_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": "4"} + ) + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.OFF + + # Test unavailable on error + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 1) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + +async def test_climate_set_temperature(hass, mock_block_device, monkeypatch): + """Test climate set temperature service.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.OFF + assert state.attributes[ATTR_TEMPERATURE] == 4 + + # Test set temperature without target temperature + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_TARGET_TEMP_LOW: 20, + ATTR_TARGET_TEMP_HIGH: 30, + }, + blocking=True, + ) + mock_block_device.http_request.assert_not_called() + + # Test set temperature + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 23}, + blocking=True, + ) + + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": "23.0"} + ) + + +async def test_climate_set_preset_mode(hass, mock_block_device, monkeypatch): + """Test climate set preset mode service.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", None) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + # Test set Profile2 + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "Profile2"}, + blocking=True, + ) + + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"schedule": 1, "schedule_profile": "2"} + ) + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", 2) + mock_block_device.mock_update() + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_PRESET_MODE] == "Profile2" + + # Set preset to none + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + assert len(mock_block_device.http_request.mock_calls) == 2 + mock_block_device.http_request.assert_called_with( + "get", "thermostat/0", {"schedule": 0} + ) + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", 0) + mock_block_device.mock_update() + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + +async def test_block_restored_climate(hass, mock_block_device, device_reg, monkeypatch): + """Test block restored climate.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + + # Partial update, should not change state + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.OFF + + +async def test_block_restored_climate_unavailable( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored climate unavailable state.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + mock_restore_cache(hass, [State(entity_id, STATE_UNAVAILABLE)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.OFF + + +async def test_block_restored_climate_set_preset_before_online( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored climate set preset before device is online.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == HVACMode.HEAT + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "Profile1"}, + blocking=True, + ) + + mock_block_device.http_request.assert_not_called() + + +async def test_block_set_mode_connection_error(hass, mock_block_device, monkeypatch): + """Test block device set mode connection error.""" + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + monkeypatch.setattr( + mock_block_device, + "http_request", + AsyncMock(side_effect=DeviceConnectionError), + ) + await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + + +async def test_block_set_mode_auth_error(hass, mock_block_device, monkeypatch): + """Test block device set mode authentication error.""" + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + monkeypatch.setattr( + mock_block_device, + "http_request", + AsyncMock(side_effect=InvalidAuthError), + ) + entry = await init_integration(hass, 1, sleep_period=1000) + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_block_restored_climate_auth_error( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored climate with authentication error during init.""" + monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, + CLIMATE_DOMAIN, + "test_name", + "sensor_0", + entry, + ) + mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) + + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + # Make device online with auth error + monkeypatch.setattr(mock_block_device, "initialized", True) + type(mock_block_device).settings = PropertyMock( + return_value={}, side_effect=InvalidAuthError + ) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id