From 232ff4f71a3be0072cd17cf00c625e16a697b169 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sat, 24 Aug 2024 12:05:53 +0200 Subject: [PATCH 01/30] chore: Add tests for BSBLAN climate component --- tests/components/bsblan/test_climate.py | 186 ++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 tests/components/bsblan/test_climate.py diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py new file mode 100644 index 00000000000000..6971130af61b4b --- /dev/null +++ b/tests/components/bsblan/test_climate.py @@ -0,0 +1,186 @@ +"""Tests for the BSBLAN climate component.""" + +from unittest.mock import AsyncMock, MagicMock + +from bsblan import BSBLANError, State +import pytest + +from homeassistant.components.bsblan.climate import BSBLANClimate +from homeassistant.components.bsblan.coordinator import BSBLanCoordinatorData +from homeassistant.components.climate import PRESET_ECO, PRESET_NONE, HVACMode +from homeassistant.const import UnitOfTemperature +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError + + +class MockStateValue: + """Mock class for a BSBLan State value.""" + + def __init__(self, value): + """Initialize the mock class.""" + self.value = value + + +@pytest.fixture +def mock_state(): + """Create a mock BSBLan State object.""" + state = MagicMock(spec=State) + state.current_temperature = MockStateValue("22.5") + state.target_temperature = MockStateValue("21.0") + state.hvac_mode = MockStateValue("heat") + return state + + +@pytest.fixture +def coordinator_mock(mock_state): + """Create a mock BSBLan coordinator.""" + coordinator = AsyncMock() + coordinator.data = BSBLanCoordinatorData(state=mock_state()) + return coordinator + + +@pytest.fixture +def mock_bsblan_data(coordinator_mock): + """Create a mock BSBLanData object.""" + + # Create a mock BSBLanData object + class MockBSBLanData: + coordinator = coordinator_mock + device = AsyncMock() + device.MAC = "00:11:22:33:44:55" + info = AsyncMock() + static = AsyncMock() + static.min_temp.value = "10.0" + static.max_temp.value = "30.0" + static.min_temp.unit = "°C" + + return MockBSBLanData() + + +@pytest.fixture +def climate(mock_bsblan_data): + """Create a BSBLANClimate object.""" + return BSBLANClimate(mock_bsblan_data) + + +async def test_current_temperature_missing(climate, coordinator_mock): + """Test the current temperature property when the value is missing.""" + coordinator_mock.data.state.current_temperature.value = "---" + assert climate.current_temperature is None + + +async def test_current_temperature_valid(climate, coordinator_mock): + """Test the current temperature property when the value is valid.""" + coordinator_mock.data.state.current_temperature.value = "22.5" + assert climate.current_temperature == 22.5 + + +async def test_target_temperature(climate, coordinator_mock): + """Test the target temperature property.""" + coordinator_mock.data.state.target_temperature.value = "21.0" + assert climate.target_temperature == 21.0 + + +async def test_hvac_mode(climate, coordinator_mock): + """Test the hvac mode property.""" + coordinator_mock.data.state.hvac_mode.value = HVACMode.HEAT + assert climate.hvac_mode == HVACMode.HEAT + + coordinator_mock.data.state.hvac_mode.value = PRESET_ECO + assert climate.hvac_mode == HVACMode.AUTO + + +async def test_preset_mode(climate, coordinator_mock): + """Test the preset mode property.""" + coordinator_mock.data.state.hvac_mode.value = HVACMode.AUTO + assert climate.preset_mode == PRESET_NONE + + coordinator_mock.data.state.hvac_mode.value = PRESET_ECO + assert climate.preset_mode == PRESET_ECO + + +async def test_async_set_hvac_mode(climate): + """Test setting the hvac mode.""" + climate.async_set_data = AsyncMock() + await climate.async_set_hvac_mode(HVACMode.HEAT) + climate.async_set_data.assert_called_once_with(hvac_mode=HVACMode.HEAT) + + +async def test_async_set_preset_mode_auto(climate, coordinator_mock): + """Test setting the preset mode when the hvac mode is auto.""" + climate.async_set_data = AsyncMock() + coordinator_mock.data.state.hvac_mode.value = HVACMode.AUTO + await climate.async_set_preset_mode(PRESET_ECO) + climate.async_set_data.assert_called_once_with(preset_mode=PRESET_ECO) + + +async def test_async_set_preset_mode_not_auto(climate, coordinator_mock): + """Test setting the preset mode when the hvac mode is not auto.""" + climate.async_set_data = AsyncMock() + coordinator_mock.data.state.hvac_mode.value = HVACMode.HEAT + with pytest.raises(ServiceValidationError): + await climate.async_set_preset_mode(PRESET_ECO) + + +async def test_async_set_temperature(climate): + """Test setting the temperature.""" + climate.async_set_data = AsyncMock() + await climate.async_set_temperature(temperature=22.0) + climate.async_set_data.assert_called_once_with(temperature=22.0) + + +async def test_async_set_data_temperature(climate): + """Test setting the temperature.""" + climate.coordinator.client.thermostat = AsyncMock() + climate.coordinator.async_request_refresh = AsyncMock() + await climate.async_set_data(temperature=22.0) + climate.coordinator.client.thermostat.assert_called_once_with( + target_temperature=22.0 + ) + climate.coordinator.async_request_refresh.assert_called_once() + + +async def test_async_set_data_hvac_mode(climate): + """Test setting the hvac mode.""" + climate.coordinator.client.thermostat = AsyncMock() + climate.coordinator.async_request_refresh = AsyncMock() + await climate.async_set_data(hvac_mode=HVACMode.HEAT) + climate.coordinator.client.thermostat.assert_called_once_with( + hvac_mode=HVACMode.HEAT + ) + climate.coordinator.async_request_refresh.assert_called_once() + + +async def test_async_set_data_preset_mode(climate): + """Test setting the preset mode.""" + climate.coordinator.client.thermostat = AsyncMock() + climate.coordinator.async_request_refresh = AsyncMock() + await climate.async_set_data(preset_mode=PRESET_ECO) + climate.coordinator.client.thermostat.assert_called_once_with(hvac_mode=PRESET_ECO) + climate.coordinator.async_request_refresh.assert_called_once() + + +async def test_async_set_data_preset_mode_none(climate): + """Test setting the preset mode to none.""" + climate.coordinator.client.thermostat = AsyncMock() + climate.coordinator.async_request_refresh = AsyncMock() + await climate.async_set_data(preset_mode=PRESET_NONE) + climate.coordinator.client.thermostat.assert_called_once_with( + hvac_mode=HVACMode.AUTO + ) + climate.coordinator.async_request_refresh.assert_called_once() + + +async def test_async_set_data_error(climate): + """Test setting the data with an error.""" + climate.coordinator.client.thermostat = AsyncMock(side_effect=BSBLANError) + with pytest.raises(HomeAssistantError): + await climate.async_set_data(temperature=22.0) + + +async def test_temperature_unit(climate, mock_bsblan_data): + """Test the temperature unit property.""" + assert climate.temperature_unit == UnitOfTemperature.CELSIUS + + mock_bsblan_data.static.min_temp.unit = "F" + climate = BSBLANClimate(mock_bsblan_data) + assert climate.temperature_unit == UnitOfTemperature.FAHRENHEIT From d95fbcbd1d02a822a32e63384a3299ebab92d2e2 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sat, 24 Aug 2024 12:48:55 +0200 Subject: [PATCH 02/30] fix return types --- tests/components/bsblan/test_climate.py | 40 ++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index 6971130af61b4b..f5e4dcedd7ede9 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -15,13 +15,13 @@ class MockStateValue: """Mock class for a BSBLan State value.""" - def __init__(self, value): + def __init__(self, value) -> None: """Initialize the mock class.""" self.value = value @pytest.fixture -def mock_state(): +def mock_state() -> MagicMock: """Create a mock BSBLan State object.""" state = MagicMock(spec=State) state.current_temperature = MockStateValue("22.5") @@ -31,7 +31,7 @@ def mock_state(): @pytest.fixture -def coordinator_mock(mock_state): +def coordinator_mock(mock_state) -> MagicMock: """Create a mock BSBLan coordinator.""" coordinator = AsyncMock() coordinator.data = BSBLanCoordinatorData(state=mock_state()) @@ -39,7 +39,7 @@ def coordinator_mock(mock_state): @pytest.fixture -def mock_bsblan_data(coordinator_mock): +def mock_bsblan_data(coordinator_mock) -> MagicMock: """Create a mock BSBLanData object.""" # Create a mock BSBLanData object @@ -57,30 +57,30 @@ class MockBSBLanData: @pytest.fixture -def climate(mock_bsblan_data): +def climate(mock_bsblan_data) -> BSBLANClimate: """Create a BSBLANClimate object.""" return BSBLANClimate(mock_bsblan_data) -async def test_current_temperature_missing(climate, coordinator_mock): +async def test_current_temperature_missing(climate, coordinator_mock) -> None: """Test the current temperature property when the value is missing.""" coordinator_mock.data.state.current_temperature.value = "---" assert climate.current_temperature is None -async def test_current_temperature_valid(climate, coordinator_mock): +async def test_current_temperature_valid(climate, coordinator_mock) -> None: """Test the current temperature property when the value is valid.""" coordinator_mock.data.state.current_temperature.value = "22.5" assert climate.current_temperature == 22.5 -async def test_target_temperature(climate, coordinator_mock): +async def test_target_temperature(climate, coordinator_mock) -> None: """Test the target temperature property.""" coordinator_mock.data.state.target_temperature.value = "21.0" assert climate.target_temperature == 21.0 -async def test_hvac_mode(climate, coordinator_mock): +async def test_hvac_mode(climate, coordinator_mock) -> None: """Test the hvac mode property.""" coordinator_mock.data.state.hvac_mode.value = HVACMode.HEAT assert climate.hvac_mode == HVACMode.HEAT @@ -89,7 +89,7 @@ async def test_hvac_mode(climate, coordinator_mock): assert climate.hvac_mode == HVACMode.AUTO -async def test_preset_mode(climate, coordinator_mock): +async def test_preset_mode(climate, coordinator_mock) -> None: """Test the preset mode property.""" coordinator_mock.data.state.hvac_mode.value = HVACMode.AUTO assert climate.preset_mode == PRESET_NONE @@ -98,14 +98,14 @@ async def test_preset_mode(climate, coordinator_mock): assert climate.preset_mode == PRESET_ECO -async def test_async_set_hvac_mode(climate): +async def test_async_set_hvac_mode(climate) -> None: """Test setting the hvac mode.""" climate.async_set_data = AsyncMock() await climate.async_set_hvac_mode(HVACMode.HEAT) climate.async_set_data.assert_called_once_with(hvac_mode=HVACMode.HEAT) -async def test_async_set_preset_mode_auto(climate, coordinator_mock): +async def test_async_set_preset_mode_auto(climate, coordinator_mock) -> None: """Test setting the preset mode when the hvac mode is auto.""" climate.async_set_data = AsyncMock() coordinator_mock.data.state.hvac_mode.value = HVACMode.AUTO @@ -113,7 +113,7 @@ async def test_async_set_preset_mode_auto(climate, coordinator_mock): climate.async_set_data.assert_called_once_with(preset_mode=PRESET_ECO) -async def test_async_set_preset_mode_not_auto(climate, coordinator_mock): +async def test_async_set_preset_mode_not_auto(climate, coordinator_mock) -> None: """Test setting the preset mode when the hvac mode is not auto.""" climate.async_set_data = AsyncMock() coordinator_mock.data.state.hvac_mode.value = HVACMode.HEAT @@ -121,14 +121,14 @@ async def test_async_set_preset_mode_not_auto(climate, coordinator_mock): await climate.async_set_preset_mode(PRESET_ECO) -async def test_async_set_temperature(climate): +async def test_async_set_temperature(climate) -> None: """Test setting the temperature.""" climate.async_set_data = AsyncMock() await climate.async_set_temperature(temperature=22.0) climate.async_set_data.assert_called_once_with(temperature=22.0) -async def test_async_set_data_temperature(climate): +async def test_async_set_data_temperature(climate) -> None: """Test setting the temperature.""" climate.coordinator.client.thermostat = AsyncMock() climate.coordinator.async_request_refresh = AsyncMock() @@ -139,7 +139,7 @@ async def test_async_set_data_temperature(climate): climate.coordinator.async_request_refresh.assert_called_once() -async def test_async_set_data_hvac_mode(climate): +async def test_async_set_data_hvac_mode(climate) -> None: """Test setting the hvac mode.""" climate.coordinator.client.thermostat = AsyncMock() climate.coordinator.async_request_refresh = AsyncMock() @@ -150,7 +150,7 @@ async def test_async_set_data_hvac_mode(climate): climate.coordinator.async_request_refresh.assert_called_once() -async def test_async_set_data_preset_mode(climate): +async def test_async_set_data_preset_mode(climate) -> None: """Test setting the preset mode.""" climate.coordinator.client.thermostat = AsyncMock() climate.coordinator.async_request_refresh = AsyncMock() @@ -159,7 +159,7 @@ async def test_async_set_data_preset_mode(climate): climate.coordinator.async_request_refresh.assert_called_once() -async def test_async_set_data_preset_mode_none(climate): +async def test_async_set_data_preset_mode_none(climate) -> None: """Test setting the preset mode to none.""" climate.coordinator.client.thermostat = AsyncMock() climate.coordinator.async_request_refresh = AsyncMock() @@ -170,14 +170,14 @@ async def test_async_set_data_preset_mode_none(climate): climate.coordinator.async_request_refresh.assert_called_once() -async def test_async_set_data_error(climate): +async def test_async_set_data_error(climate) -> None: """Test setting the data with an error.""" climate.coordinator.client.thermostat = AsyncMock(side_effect=BSBLANError) with pytest.raises(HomeAssistantError): await climate.async_set_data(temperature=22.0) -async def test_temperature_unit(climate, mock_bsblan_data): +async def test_temperature_unit(climate, mock_bsblan_data) -> None: """Test the temperature unit property.""" assert climate.temperature_unit == UnitOfTemperature.CELSIUS From 813315ce21fb8f16806c6f3bad8171c81367994c Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Mon, 26 Aug 2024 10:24:03 +0200 Subject: [PATCH 03/30] fix MAC data --- homeassistant/components/bsblan/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bsblan/entity.py b/homeassistant/components/bsblan/entity.py index 252c397f4f2638..9ffc8f32125e81 100644 --- a/homeassistant/components/bsblan/entity.py +++ b/homeassistant/components/bsblan/entity.py @@ -22,7 +22,7 @@ class BSBLanEntity(CoordinatorEntity[BSBLanUpdateCoordinator]): def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None: """Initialize BSBLan entity.""" super().__init__(coordinator, data) - host = coordinator.config_entry.data["host"] + host = self.coordinator.config_entry.data["host"] mac = data.device.MAC self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mac)}, From f5aabf8593e19cabae910658ba1b7b865cec1841 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Mon, 26 Aug 2024 10:36:46 +0200 Subject: [PATCH 04/30] chore: Update BSBLAN climate component tests used setup from conftest added setup for farhenheit temp unit --- tests/components/bsblan/conftest.py | 49 +++++- tests/components/bsblan/test_climate.py | 222 ++++++++---------------- 2 files changed, 116 insertions(+), 155 deletions(-) diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 13d4017d7c8bbf..9e1759abfd3667 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -6,11 +6,17 @@ from bsblan import Device, Info, State, StaticState import pytest +from homeassistant.components.bsblan import BSBLanData +from homeassistant.components.bsblan.climate import BSBLANClimate from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN +from homeassistant.components.bsblan.coordinator import ( + BSBLanCoordinatorData, + BSBLanUpdateCoordinator, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_fixture, mock_device_registry @pytest.fixture @@ -71,3 +77,44 @@ async def init_integration( await hass.async_block_till_done() return mock_config_entry + + +@pytest.fixture +def climate(hass: HomeAssistant, mock_config_entry, mock_bsblan) -> BSBLANClimate: + """Set up the BSBLan climate entity for testing.""" + mock_device_registry(hass) + coordinator = BSBLanUpdateCoordinator(hass, mock_config_entry, mock_bsblan) + coordinator.config_entry = mock_config_entry + coordinator.data = BSBLanCoordinatorData(state=mock_bsblan.state.return_value) + data = BSBLanData( + client=mock_bsblan, + coordinator=coordinator, + device=mock_bsblan.device.return_value, + info=mock_bsblan.info.return_value, + static=mock_bsblan.static_values.return_value, + ) + hass.data.setdefault("bsblan", {})[mock_config_entry.entry_id] = data + return BSBLANClimate(data) + + +@pytest.fixture +def climate_fahrenheit( + hass: HomeAssistant, mock_config_entry, mock_bsblan +) -> BSBLANClimate: + """Set up the BSBLan climate entity with Fahrenheit temperature unit.""" + mock_device_registry(hass) + coordinator = BSBLanUpdateCoordinator(hass, mock_config_entry, mock_bsblan) + coordinator.config_entry = mock_config_entry + coordinator.data = BSBLanCoordinatorData(state=mock_bsblan.state.return_value) + # override min_temp unit to Fahrenheit in mock_bsblan static values + mock_bsblan.static_values.return_value.min_temp.unit = "°F" + + data = BSBLanData( + client=mock_bsblan, + coordinator=coordinator, + device=mock_bsblan.device.return_value, + info=mock_bsblan.info.return_value, + static=mock_bsblan.static_values.return_value, + ) + hass.data.setdefault("bsblan", {})[mock_config_entry.entry_id] = data + return BSBLANClimate(data) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index f5e4dcedd7ede9..23f8ce1b79fec7 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -1,186 +1,100 @@ -"""Tests for the BSBLAN climate component.""" +"""Tests for the BSB-Lan climate platform.""" -from unittest.mock import AsyncMock, MagicMock - -from bsblan import BSBLANError, State +from bsblan import BSBLANError import pytest -from homeassistant.components.bsblan.climate import BSBLANClimate -from homeassistant.components.bsblan.coordinator import BSBLanCoordinatorData -from homeassistant.components.climate import PRESET_ECO, PRESET_NONE, HVACMode +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, + PRESET_ECO, + PRESET_NONE, + HVACMode, +) from homeassistant.const import UnitOfTemperature -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError - - -class MockStateValue: - """Mock class for a BSBLan State value.""" - - def __init__(self, value) -> None: - """Initialize the mock class.""" - self.value = value - - -@pytest.fixture -def mock_state() -> MagicMock: - """Create a mock BSBLan State object.""" - state = MagicMock(spec=State) - state.current_temperature = MockStateValue("22.5") - state.target_temperature = MockStateValue("21.0") - state.hvac_mode = MockStateValue("heat") - return state - - -@pytest.fixture -def coordinator_mock(mock_state) -> MagicMock: - """Create a mock BSBLan coordinator.""" - coordinator = AsyncMock() - coordinator.data = BSBLanCoordinatorData(state=mock_state()) - return coordinator - - -@pytest.fixture -def mock_bsblan_data(coordinator_mock) -> MagicMock: - """Create a mock BSBLanData object.""" +from homeassistant.exceptions import HomeAssistantError - # Create a mock BSBLanData object - class MockBSBLanData: - coordinator = coordinator_mock - device = AsyncMock() - device.MAC = "00:11:22:33:44:55" - info = AsyncMock() - static = AsyncMock() - static.min_temp.value = "10.0" - static.max_temp.value = "30.0" - static.min_temp.unit = "°C" - return MockBSBLanData() - - -@pytest.fixture -def climate(mock_bsblan_data) -> BSBLANClimate: - """Create a BSBLANClimate object.""" - return BSBLANClimate(mock_bsblan_data) - - -async def test_current_temperature_missing(climate, coordinator_mock) -> None: - """Test the current temperature property when the value is missing.""" - coordinator_mock.data.state.current_temperature.value = "---" - assert climate.current_temperature is None - - -async def test_current_temperature_valid(climate, coordinator_mock) -> None: - """Test the current temperature property when the value is valid.""" - coordinator_mock.data.state.current_temperature.value = "22.5" - assert climate.current_temperature == 22.5 +async def test_climate_init(climate) -> None: + """Test initialization of the climate entity.""" + assert climate.unique_id == "00:80:41:19:69:90-climate" + assert climate.temperature_unit == UnitOfTemperature.CELSIUS + assert climate.min_temp == 8.0 + assert climate.max_temp == 20.0 -async def test_target_temperature(climate, coordinator_mock) -> None: - """Test the target temperature property.""" - coordinator_mock.data.state.target_temperature.value = "21.0" - assert climate.target_temperature == 21.0 +def test_temperature_unit_assignment(climate_fahrenheit) -> None: + """Test the temperature unit assignment based on the static data.""" + assert climate_fahrenheit._attr_temperature_unit == UnitOfTemperature.FAHRENHEIT -async def test_hvac_mode(climate, coordinator_mock) -> None: - """Test the hvac mode property.""" - coordinator_mock.data.state.hvac_mode.value = HVACMode.HEAT +async def test_climate_properties(climate, mock_bsblan) -> None: + """Test properties of the climate entity.""" + assert climate.current_temperature == 18.6 + assert climate.target_temperature == 18.5 assert climate.hvac_mode == HVACMode.HEAT - - coordinator_mock.data.state.hvac_mode.value = PRESET_ECO - assert climate.hvac_mode == HVACMode.AUTO - - -async def test_preset_mode(climate, coordinator_mock) -> None: - """Test the preset mode property.""" - coordinator_mock.data.state.hvac_mode.value = HVACMode.AUTO assert climate.preset_mode == PRESET_NONE - coordinator_mock.data.state.hvac_mode.value = PRESET_ECO + mock_bsblan.state.return_value.current_temperature.value = "---" + assert climate.current_temperature is None + + mock_bsblan.state.return_value.hvac_mode.value = PRESET_ECO assert climate.preset_mode == PRESET_ECO + assert climate.hvac_mode == HVACMode.AUTO -async def test_async_set_hvac_mode(climate) -> None: - """Test setting the hvac mode.""" - climate.async_set_data = AsyncMock() +async def test_climate_set_hvac_mode(climate, mock_bsblan) -> None: + """Test setting the HVAC mode.""" await climate.async_set_hvac_mode(HVACMode.HEAT) - climate.async_set_data.assert_called_once_with(hvac_mode=HVACMode.HEAT) + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.HEAT) -async def test_async_set_preset_mode_auto(climate, coordinator_mock) -> None: - """Test setting the preset mode when the hvac mode is auto.""" - climate.async_set_data = AsyncMock() - coordinator_mock.data.state.hvac_mode.value = HVACMode.AUTO - await climate.async_set_preset_mode(PRESET_ECO) - climate.async_set_data.assert_called_once_with(preset_mode=PRESET_ECO) +async def test_climate_set_preset_mode(climate, mock_bsblan) -> None: + """Test setting the preset mode.""" + mock_bsblan.state.return_value.hvac_mode.value = HVACMode.AUTO + await climate.async_set_preset_mode(PRESET_NONE) + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.AUTO) -async def test_async_set_preset_mode_not_auto(climate, coordinator_mock) -> None: - """Test setting the preset mode when the hvac mode is not auto.""" - climate.async_set_data = AsyncMock() - coordinator_mock.data.state.hvac_mode.value = HVACMode.HEAT - with pytest.raises(ServiceValidationError): - await climate.async_set_preset_mode(PRESET_ECO) +async def test_climate_set_temperature(climate, mock_bsblan) -> None: + """Test setting the target temperature.""" + await climate.async_set_temperature(**{ATTR_TEMPERATURE: 20}) + mock_bsblan.thermostat.assert_called_once_with(target_temperature=20) -async def test_async_set_temperature(climate) -> None: - """Test setting the temperature.""" - climate.async_set_data = AsyncMock() - await climate.async_set_temperature(temperature=22.0) - climate.async_set_data.assert_called_once_with(temperature=22.0) +async def test_climate_set_data_error(climate, mock_bsblan) -> None: + """Test error while setting data.""" + mock_bsblan.thermostat.side_effect = BSBLANError + with pytest.raises(HomeAssistantError): + await climate.async_set_temperature(**{ATTR_TEMPERATURE: 20}) -async def test_async_set_data_temperature(climate) -> None: - """Test setting the temperature.""" - climate.coordinator.client.thermostat = AsyncMock() - climate.coordinator.async_request_refresh = AsyncMock() - await climate.async_set_data(temperature=22.0) - climate.coordinator.client.thermostat.assert_called_once_with( - target_temperature=22.0 - ) - climate.coordinator.async_request_refresh.assert_called_once() +async def test_climate_current_temperature_none(climate, mock_bsblan) -> None: + """Test when the current temperature value is '---'.""" + mock_bsblan.state.return_value.current_temperature.value = "---" + assert climate.current_temperature is None -async def test_async_set_data_hvac_mode(climate) -> None: - """Test setting the hvac mode.""" - climate.coordinator.client.thermostat = AsyncMock() - climate.coordinator.async_request_refresh = AsyncMock() - await climate.async_set_data(hvac_mode=HVACMode.HEAT) - climate.coordinator.client.thermostat.assert_called_once_with( - hvac_mode=HVACMode.HEAT - ) - climate.coordinator.async_request_refresh.assert_called_once() +async def test_climate_turn_on(climate, mock_bsblan) -> None: + """Test turning on the climate entity.""" + await climate.async_turn_on() + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.HEAT) -async def test_async_set_data_preset_mode(climate) -> None: - """Test setting the preset mode.""" - climate.coordinator.client.thermostat = AsyncMock() - climate.coordinator.async_request_refresh = AsyncMock() - await climate.async_set_data(preset_mode=PRESET_ECO) - climate.coordinator.client.thermostat.assert_called_once_with(hvac_mode=PRESET_ECO) - climate.coordinator.async_request_refresh.assert_called_once() - - -async def test_async_set_data_preset_mode_none(climate) -> None: - """Test setting the preset mode to none.""" - climate.coordinator.client.thermostat = AsyncMock() - climate.coordinator.async_request_refresh = AsyncMock() - await climate.async_set_data(preset_mode=PRESET_NONE) - climate.coordinator.client.thermostat.assert_called_once_with( - hvac_mode=HVACMode.AUTO - ) - climate.coordinator.async_request_refresh.assert_called_once() - - -async def test_async_set_data_error(climate) -> None: - """Test setting the data with an error.""" - climate.coordinator.client.thermostat = AsyncMock(side_effect=BSBLANError) - with pytest.raises(HomeAssistantError): - await climate.async_set_data(temperature=22.0) +async def test_climate_turn_off(climate, mock_bsblan) -> None: + """Test turning off the climate entity.""" + await climate.async_turn_off() + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.OFF) -async def test_temperature_unit(climate, mock_bsblan_data) -> None: - """Test the temperature unit property.""" - assert climate.temperature_unit == UnitOfTemperature.CELSIUS +async def test_climate_set_preset_mode_eco(climate, mock_bsblan) -> None: + """Test setting the preset mode to eco.""" + mock_bsblan.state.return_value.hvac_mode.value = HVACMode.AUTO + await climate.async_set_preset_mode(PRESET_ECO) + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=PRESET_ECO) + assert climate.hvac_mode == HVACMode.AUTO + - mock_bsblan_data.static.min_temp.unit = "F" - climate = BSBLANClimate(mock_bsblan_data) - assert climate.temperature_unit == UnitOfTemperature.FAHRENHEIT +async def test_climate_set_preset_mode_error(climate, mock_bsblan) -> None: + """Test setting the preset mode when it fails with a BSBLANError.""" + mock_bsblan.thermostat.side_effect = BSBLANError + with pytest.raises(HomeAssistantError): + await climate.async_set_preset_mode(PRESET_ECO) From 6124660c7e2b38ecbcadbbcefb1881f1b3fcded1 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Mon, 26 Aug 2024 11:07:10 +0200 Subject: [PATCH 05/30] chore: Update BSBLAN climate component tests use syrupy to compare results --- tests/components/bsblan/__init__.py | 17 +++ .../bsblan/snapshots/test_climate.ambr | 74 ++++++++++++ tests/components/bsblan/test_climate.py | 108 +++--------------- 3 files changed, 108 insertions(+), 91 deletions(-) create mode 100644 tests/components/bsblan/snapshots/test_climate.ambr diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py index d233fa068ea95e..239e6ca7b3521b 100644 --- a/tests/components/bsblan/__init__.py +++ b/tests/components/bsblan/__init__.py @@ -1 +1,18 @@ """Tests for the bsblan integration.""" + +from unittest.mock import patch + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_with_selected_platforms( + hass: HomeAssistant, entry: MockConfigEntry, platforms: list[Platform] +) -> None: + """Set up the BSBLAN integration with the selected platforms.""" + entry.add_to_hass(hass) + with patch("homeassistant.components.bsblan.PLATFORMS", platforms): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/bsblan/snapshots/test_climate.ambr b/tests/components/bsblan/snapshots/test_climate.ambr new file mode 100644 index 00000000000000..73354cc4f19e0d --- /dev/null +++ b/tests/components/bsblan/snapshots/test_climate.ambr @@ -0,0 +1,74 @@ +# serializer version: 1 +# name: test_climate_entity[climate.bsb_lan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 20.0, + 'min_temp': 8.0, + 'preset_modes': list([ + 'eco', + 'none', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.bsb_lan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:80:41:19:69:90-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_entity[climate.bsb_lan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 18.6, + 'friendly_name': 'BSB-LAN', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 20.0, + 'min_temp': 8.0, + 'preset_mode': 'none', + 'preset_modes': list([ + 'eco', + 'none', + ]), + 'supported_features': , + 'temperature': 18.5, + }), + 'context': , + 'entity_id': 'climate.bsb_lan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index 23f8ce1b79fec7..0c9524413e6a7c 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -1,100 +1,26 @@ """Tests for the BSB-Lan climate platform.""" -from bsblan import BSBLANError -import pytest +from unittest.mock import AsyncMock -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, - PRESET_ECO, - PRESET_NONE, - HVACMode, -) -from homeassistant.const import UnitOfTemperature -from homeassistant.exceptions import HomeAssistantError +from syrupy.assertion import SnapshotAssertion +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er -async def test_climate_init(climate) -> None: - """Test initialization of the climate entity.""" - assert climate.unique_id == "00:80:41:19:69:90-climate" - assert climate.temperature_unit == UnitOfTemperature.CELSIUS - assert climate.min_temp == 8.0 - assert climate.max_temp == 20.0 +from . import setup_with_selected_platforms +from tests.common import MockConfigEntry, snapshot_platform -def test_temperature_unit_assignment(climate_fahrenheit) -> None: - """Test the temperature unit assignment based on the static data.""" - assert climate_fahrenheit._attr_temperature_unit == UnitOfTemperature.FAHRENHEIT +async def test_climate_entity( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test the initial parameters.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) -async def test_climate_properties(climate, mock_bsblan) -> None: - """Test properties of the climate entity.""" - assert climate.current_temperature == 18.6 - assert climate.target_temperature == 18.5 - assert climate.hvac_mode == HVACMode.HEAT - assert climate.preset_mode == PRESET_NONE - - mock_bsblan.state.return_value.current_temperature.value = "---" - assert climate.current_temperature is None - - mock_bsblan.state.return_value.hvac_mode.value = PRESET_ECO - assert climate.preset_mode == PRESET_ECO - assert climate.hvac_mode == HVACMode.AUTO - - -async def test_climate_set_hvac_mode(climate, mock_bsblan) -> None: - """Test setting the HVAC mode.""" - await climate.async_set_hvac_mode(HVACMode.HEAT) - mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.HEAT) - - -async def test_climate_set_preset_mode(climate, mock_bsblan) -> None: - """Test setting the preset mode.""" - mock_bsblan.state.return_value.hvac_mode.value = HVACMode.AUTO - await climate.async_set_preset_mode(PRESET_NONE) - mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.AUTO) - - -async def test_climate_set_temperature(climate, mock_bsblan) -> None: - """Test setting the target temperature.""" - await climate.async_set_temperature(**{ATTR_TEMPERATURE: 20}) - mock_bsblan.thermostat.assert_called_once_with(target_temperature=20) - - -async def test_climate_set_data_error(climate, mock_bsblan) -> None: - """Test error while setting data.""" - mock_bsblan.thermostat.side_effect = BSBLANError - with pytest.raises(HomeAssistantError): - await climate.async_set_temperature(**{ATTR_TEMPERATURE: 20}) - - -async def test_climate_current_temperature_none(climate, mock_bsblan) -> None: - """Test when the current temperature value is '---'.""" - mock_bsblan.state.return_value.current_temperature.value = "---" - assert climate.current_temperature is None - - -async def test_climate_turn_on(climate, mock_bsblan) -> None: - """Test turning on the climate entity.""" - await climate.async_turn_on() - mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.HEAT) - - -async def test_climate_turn_off(climate, mock_bsblan) -> None: - """Test turning off the climate entity.""" - await climate.async_turn_off() - mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.OFF) - - -async def test_climate_set_preset_mode_eco(climate, mock_bsblan) -> None: - """Test setting the preset mode to eco.""" - mock_bsblan.state.return_value.hvac_mode.value = HVACMode.AUTO - await climate.async_set_preset_mode(PRESET_ECO) - mock_bsblan.thermostat.assert_called_once_with(hvac_mode=PRESET_ECO) - assert climate.hvac_mode == HVACMode.AUTO - - -async def test_climate_set_preset_mode_error(climate, mock_bsblan) -> None: - """Test setting the preset mode when it fails with a BSBLANError.""" - mock_bsblan.thermostat.side_effect = BSBLANError - with pytest.raises(HomeAssistantError): - await climate.async_set_preset_mode(PRESET_ECO) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From fc378f2063f0a9913230697456b7852496b2f266 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sat, 31 Aug 2024 20:34:04 +0200 Subject: [PATCH 06/30] add test for temp_unit --- tests/components/bsblan/conftest.py | 56 +------ .../components/bsblan/fixtures/static_F.json | 20 +++ .../bsblan/snapshots/test_climate.ambr | 146 +++++++++++++++++ .../bsblan/snapshots/test_diagnostics.ambr | 154 ++++++++++++++++++ tests/components/bsblan/test_diagnostics.py | 2 + 5 files changed, 326 insertions(+), 52 deletions(-) create mode 100644 tests/components/bsblan/fixtures/static_F.json diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 9e1759abfd3667..c0aa40adbf2c21 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -6,17 +6,11 @@ from bsblan import Device, Info, State, StaticState import pytest -from homeassistant.components.bsblan import BSBLanData -from homeassistant.components.bsblan.climate import BSBLANClimate from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN -from homeassistant.components.bsblan.coordinator import ( - BSBLanCoordinatorData, - BSBLanUpdateCoordinator, -) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture, mock_device_registry +from tests.common import MockConfigEntry, load_fixture @pytest.fixture @@ -45,8 +39,8 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_setup -@pytest.fixture -def mock_bsblan() -> Generator[MagicMock]: +@pytest.fixture(params=["static.json", "static_F.json"]) +def mock_bsblan(request: pytest.FixtureRequest) -> Generator[MagicMock, None, None]: """Return a mocked BSBLAN client.""" with ( patch("homeassistant.components.bsblan.BSBLAN", autospec=True) as bsblan_mock, @@ -60,9 +54,8 @@ def mock_bsblan() -> Generator[MagicMock]: bsblan.state.return_value = State.from_json(load_fixture("state.json", DOMAIN)) bsblan.static_values.return_value = StaticState.from_json( - load_fixture("static.json", DOMAIN) + load_fixture(request.param, DOMAIN) ) - yield bsblan @@ -77,44 +70,3 @@ async def init_integration( await hass.async_block_till_done() return mock_config_entry - - -@pytest.fixture -def climate(hass: HomeAssistant, mock_config_entry, mock_bsblan) -> BSBLANClimate: - """Set up the BSBLan climate entity for testing.""" - mock_device_registry(hass) - coordinator = BSBLanUpdateCoordinator(hass, mock_config_entry, mock_bsblan) - coordinator.config_entry = mock_config_entry - coordinator.data = BSBLanCoordinatorData(state=mock_bsblan.state.return_value) - data = BSBLanData( - client=mock_bsblan, - coordinator=coordinator, - device=mock_bsblan.device.return_value, - info=mock_bsblan.info.return_value, - static=mock_bsblan.static_values.return_value, - ) - hass.data.setdefault("bsblan", {})[mock_config_entry.entry_id] = data - return BSBLANClimate(data) - - -@pytest.fixture -def climate_fahrenheit( - hass: HomeAssistant, mock_config_entry, mock_bsblan -) -> BSBLANClimate: - """Set up the BSBLan climate entity with Fahrenheit temperature unit.""" - mock_device_registry(hass) - coordinator = BSBLanUpdateCoordinator(hass, mock_config_entry, mock_bsblan) - coordinator.config_entry = mock_config_entry - coordinator.data = BSBLanCoordinatorData(state=mock_bsblan.state.return_value) - # override min_temp unit to Fahrenheit in mock_bsblan static values - mock_bsblan.static_values.return_value.min_temp.unit = "°F" - - data = BSBLanData( - client=mock_bsblan, - coordinator=coordinator, - device=mock_bsblan.device.return_value, - info=mock_bsblan.info.return_value, - static=mock_bsblan.static_values.return_value, - ) - hass.data.setdefault("bsblan", {})[mock_config_entry.entry_id] = data - return BSBLANClimate(data) diff --git a/tests/components/bsblan/fixtures/static_F.json b/tests/components/bsblan/fixtures/static_F.json new file mode 100644 index 00000000000000..a61e870f6e51ef --- /dev/null +++ b/tests/components/bsblan/fixtures/static_F.json @@ -0,0 +1,20 @@ +{ + "min_temp": { + "name": "Room temp frost protection setpoint", + "error": 0, + "value": "8.0", + "desc": "", + "dataType": 0, + "readonly": 0, + "unit": "°F" + }, + "max_temp": { + "name": "Summer/winter changeover temp heat circuit 1", + "error": 0, + "value": "20.0", + "desc": "", + "dataType": 0, + "readonly": 0, + "unit": "°F" + } +} diff --git a/tests/components/bsblan/snapshots/test_climate.ambr b/tests/components/bsblan/snapshots/test_climate.ambr index 73354cc4f19e0d..14a137c9133ba6 100644 --- a/tests/components/bsblan/snapshots/test_climate.ambr +++ b/tests/components/bsblan/snapshots/test_climate.ambr @@ -72,3 +72,149 @@ 'state': 'heat', }) # --- +# name: test_climate_entity[static.json][climate.bsb_lan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 20.0, + 'min_temp': 8.0, + 'preset_modes': list([ + 'eco', + 'none', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.bsb_lan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:80:41:19:69:90-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_entity[static.json][climate.bsb_lan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 18.6, + 'friendly_name': 'BSB-LAN', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 20.0, + 'min_temp': 8.0, + 'preset_mode': 'none', + 'preset_modes': list([ + 'eco', + 'none', + ]), + 'supported_features': , + 'temperature': 18.5, + }), + 'context': , + 'entity_id': 'climate.bsb_lan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_climate_entity[static_F.json][climate.bsb_lan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': -6.7, + 'min_temp': -13.3, + 'preset_modes': list([ + 'eco', + 'none', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.bsb_lan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:80:41:19:69:90-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_entity[static_F.json][climate.bsb_lan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -7.4, + 'friendly_name': 'BSB-LAN', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': -6.7, + 'min_temp': -13.3, + 'preset_mode': 'none', + 'preset_modes': list([ + 'eco', + 'none', + ]), + 'supported_features': , + 'temperature': -7.5, + }), + 'context': , + 'entity_id': 'climate.bsb_lan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index c9a82edf4e2f86..59d84bde66b62e 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -94,3 +94,157 @@ }), }) # --- +# name: test_diagnostics[static.json] + dict({ + 'device': dict({ + 'MAC': '00:80:41:19:69:90', + 'name': 'BSB-LAN', + 'uptime': 969402857, + 'version': '1.0.38-20200730234859', + }), + 'info': dict({ + 'controller_family': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Device family', + 'unit': '', + 'value': '211', + }), + 'controller_variant': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Device variant', + 'unit': '', + 'value': '127', + }), + 'device_identification': dict({ + 'data_type': 7, + 'desc': '', + 'name': 'Gerte-Identifikation', + 'unit': '', + 'value': 'RVS21.831F/127', + }), + }), + 'state': dict({ + 'current_temperature': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temp 1 actual value', + 'unit': '°C', + 'value': '18.6', + }), + 'hvac_action': dict({ + 'data_type': 1, + 'desc': 'Raumtemp’begrenzung', + 'name': 'Status heating circuit 1', + 'unit': '', + 'value': '122', + }), + 'hvac_mode': dict({ + 'data_type': 1, + 'desc': 'Komfort', + 'name': 'Operating mode', + 'unit': '', + 'value': 'heat', + }), + 'hvac_mode2': dict({ + 'data_type': 1, + 'desc': 'Reduziert', + 'name': 'Operating mode', + 'unit': '', + 'value': '2', + }), + 'room1_thermostat_mode': dict({ + 'data_type': 1, + 'desc': 'Kein Bedarf', + 'name': 'Raumthermostat 1', + 'unit': '', + 'value': '0', + }), + 'target_temperature': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temperature Comfort setpoint', + 'unit': '°C', + 'value': '18.5', + }), + }), + }) +# --- +# name: test_diagnostics[static_F.json] + dict({ + 'device': dict({ + 'MAC': '00:80:41:19:69:90', + 'name': 'BSB-LAN', + 'uptime': 969402857, + 'version': '1.0.38-20200730234859', + }), + 'info': dict({ + 'controller_family': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Device family', + 'unit': '', + 'value': '211', + }), + 'controller_variant': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Device variant', + 'unit': '', + 'value': '127', + }), + 'device_identification': dict({ + 'data_type': 7, + 'desc': '', + 'name': 'Gerte-Identifikation', + 'unit': '', + 'value': 'RVS21.831F/127', + }), + }), + 'state': dict({ + 'current_temperature': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temp 1 actual value', + 'unit': '°C', + 'value': '18.6', + }), + 'hvac_action': dict({ + 'data_type': 1, + 'desc': 'Raumtemp’begrenzung', + 'name': 'Status heating circuit 1', + 'unit': '', + 'value': '122', + }), + 'hvac_mode': dict({ + 'data_type': 1, + 'desc': 'Komfort', + 'name': 'Operating mode', + 'unit': '', + 'value': 'heat', + }), + 'hvac_mode2': dict({ + 'data_type': 1, + 'desc': 'Reduziert', + 'name': 'Operating mode', + 'unit': '', + 'value': '2', + }), + 'room1_thermostat_mode': dict({ + 'data_type': 1, + 'desc': 'Kein Bedarf', + 'name': 'Raumthermostat 1', + 'unit': '', + 'value': '0', + }), + 'target_temperature': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temperature Comfort setpoint', + 'unit': '°C', + 'value': '18.5', + }), + }), + }) +# --- diff --git a/tests/components/bsblan/test_diagnostics.py b/tests/components/bsblan/test_diagnostics.py index 8939456c2ac012..64f4adca2e4185 100644 --- a/tests/components/bsblan/test_diagnostics.py +++ b/tests/components/bsblan/test_diagnostics.py @@ -1,5 +1,6 @@ """Tests for the diagnostics data provided by the BSBLan integration.""" +import pytest from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -9,6 +10,7 @@ from tests.typing import ClientSessionGenerator +@pytest.mark.parametrize("mock_bsblan", ["static.json", "static_F.json"], indirect=True) async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, From 0f7c6f985ed332878d176f81866f542e7937eae8 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sat, 31 Aug 2024 21:23:18 +0200 Subject: [PATCH 07/30] update climate tests set current_temperature to None in test case. Is this the correct way for testing? --- tests/components/bsblan/conftest.py | 8 ++++++++ tests/components/bsblan/test_climate.py | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index c0aa40adbf2c21..331d7881a53f97 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -56,6 +56,14 @@ def mock_bsblan(request: pytest.FixtureRequest) -> Generator[MagicMock, None, No bsblan.static_values.return_value = StaticState.from_json( load_fixture(request.param, DOMAIN) ) + + # Add a method to update the current_temperature dynamically + def set_current_temperature(value): + state = bsblan.state.return_value + state.current_temperature.value = value + + bsblan.set_current_temperature = set_current_temperature + yield bsblan diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index 0c9524413e6a7c..cc738c06d4dd09 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -24,3 +24,15 @@ async def test_climate_entity( await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Spoof the current_temperature value to "---" + mock_bsblan.set_current_temperature("---") + + # Update the state in Home Assistant + await hass.helpers.entity_component.async_update_entity("climate.bsb_lan") + + # Get the state of the climate entity + state = hass.states.get("climate.bsb_lan") + + # Assert that the current_temperature attribute is None + assert state.attributes["current_temperature"] is None From 1b8d88b2190855d95b71c0899def244b42d2d290 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sun, 1 Sep 2024 22:22:49 +0200 Subject: [PATCH 08/30] chore: Update BSBLAN diagnostics to handle asynchronous data retrieval --- homeassistant/components/bsblan/diagnostics.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py index b4ff67f4fbfe97..1023fdf97e715e 100644 --- a/homeassistant/components/bsblan/diagnostics.py +++ b/homeassistant/components/bsblan/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from typing import Any from homeassistant.config_entries import ConfigEntry @@ -18,10 +19,15 @@ async def async_get_config_entry_diagnostics( data: BSBLanData = hass.data[DOMAIN][entry.entry_id] return { - "info": data.info.to_dict(), - "device": data.device.to_dict(), + "info": await data.info.to_dict() + if asyncio.iscoroutinefunction(data.info.to_dict) + else data.info.to_dict(), + "device": await data.device.to_dict() + if asyncio.iscoroutinefunction(data.device.to_dict) + else data.device.to_dict(), "coordinator_data": { - "state": data.coordinator.data.state.to_dict(), + "state": await data.coordinator.data.state.to_dict() + if asyncio.iscoroutinefunction(data.coordinator.data.state.to_dict) + else data.coordinator.data.state.to_dict(), }, - "static": data.static.to_dict(), } From cbb5447f258a1336319a7a47b8dde780f4982a32 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sun, 1 Sep 2024 22:23:32 +0200 Subject: [PATCH 09/30] chore: Refactor BSBLAN conftest.py to simplify fixture and patching --- tests/components/bsblan/conftest.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 331d7881a53f97..6a319c5d252a2b 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -39,8 +39,8 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_setup -@pytest.fixture(params=["static.json", "static_F.json"]) -def mock_bsblan(request: pytest.FixtureRequest) -> Generator[MagicMock, None, None]: +@pytest.fixture +def mock_bsblan() -> Generator[MagicMock, None, None]: """Return a mocked BSBLAN client.""" with ( patch("homeassistant.components.bsblan.BSBLAN", autospec=True) as bsblan_mock, @@ -53,16 +53,13 @@ def mock_bsblan(request: pytest.FixtureRequest) -> Generator[MagicMock, None, No ) bsblan.state.return_value = State.from_json(load_fixture("state.json", DOMAIN)) - bsblan.static_values.return_value = StaticState.from_json( - load_fixture(request.param, DOMAIN) - ) - - # Add a method to update the current_temperature dynamically - def set_current_temperature(value): - state = bsblan.state.return_value - state.current_temperature.value = value + # Patch static values based on the test case + async def set_static_values(param): + bsblan.static_values.return_value = StaticState.from_json( + load_fixture(param, DOMAIN) + ) - bsblan.set_current_temperature = set_current_temperature + bsblan.set_static_values = set_static_values yield bsblan From 0f6f832ba5633abedc683db562074bb9028e6871 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sun, 1 Sep 2024 22:39:06 +0200 Subject: [PATCH 10/30] chore: Update BSBLAN climate component tests 100% test coverage --- tests/components/bsblan/test_climate.py | 286 ++++++++++++++++++++++-- 1 file changed, 273 insertions(+), 13 deletions(-) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index cc738c06d4dd09..da0f165a699a18 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -1,38 +1,298 @@ """Tests for the BSB-Lan climate platform.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, PropertyMock, patch +from bsblan import BSBLANError +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.bsblan.climate import BSBLANClimate +from homeassistant.components.bsblan.const import ATTR_TARGET_TEMPERATURE +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + DOMAIN as CLIMATE_DOMAIN, + PRESET_ECO, + PRESET_NONE, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.entity_registry as er from . import setup_with_selected_platforms from tests.common import MockConfigEntry, snapshot_platform +ENTITY_ID = "climate.bsb_lan" -async def test_climate_entity( + +@pytest.mark.parametrize("static_file", ["static.json"]) +async def test_climate_entity_properties( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + static_file: str, ) -> None: - """Test the initial parameters.""" + """Test the initial parameters in Celsius.""" + await mock_bsblan.set_static_values(static_file) await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - # Spoof the current_temperature value to "---" - mock_bsblan.set_current_temperature("---") + climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) + + # Test when current_temperature is "---" + with patch.object( + mock_bsblan.state.return_value.current_temperature, "value", "---" + ): + await hass.helpers.entity_component.async_update_entity(ENTITY_ID) + state = hass.states.get(ENTITY_ID) + assert state.attributes["current_temperature"] is None + + # Test target_temperature + target_temperature = 16.2 + with patch.object( + mock_bsblan.state.return_value.target_temperature, "value", target_temperature + ): + await hass.helpers.entity_component.async_update_entity(ENTITY_ID) + state = hass.states.get(ENTITY_ID) + assert state.attributes["temperature"] == target_temperature + + # Test hvac_mode when preset_mode is ECO + with patch.object(mock_bsblan.state.return_value.hvac_mode, "value", PRESET_ECO): + assert climate_entity.hvac_mode == HVACMode.AUTO + + # Test hvac_mode with other values + with patch.object(mock_bsblan.state.return_value.hvac_mode, "value", HVACMode.HEAT): + assert climate_entity.hvac_mode == HVACMode.HEAT + + # Test preset_mode + with patch.object( + BSBLANClimate, "hvac_mode", new_callable=PropertyMock + ) as mock_hvac_mode: + mock_hvac_mode.return_value = HVACMode.AUTO + mock_bsblan.state.return_value.hvac_mode.value = PRESET_ECO + await hass.helpers.entity_component.async_update_entity(ENTITY_ID) + state = hass.states.get(ENTITY_ID) + assert state.attributes["preset_mode"] == PRESET_ECO + + +@pytest.mark.parametrize("static_file", ["static.json"]) +@pytest.mark.parametrize( + ("mode", "expected_call"), + [ + (HVACMode.HEAT, HVACMode.HEAT), + (HVACMode.COOL, HVACMode.COOL), + (HVACMode.AUTO, HVACMode.AUTO), + (HVACMode.OFF, HVACMode.OFF), + ], +) +async def test_async_set_hvac_mode( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + static_file: str, + mode: str, + expected_call: str, +) -> None: + """Test the async_set_hvac_mode function.""" + await mock_bsblan.set_static_values(static_file) + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) + + with patch.object(climate_entity, "async_set_data") as mock_set_data: + await climate_entity.async_set_hvac_mode(mode) + mock_set_data.assert_called_once_with(hvac_mode=expected_call) + + +@pytest.mark.parametrize("static_file", ["static.json"]) +@pytest.mark.parametrize( + ("hvac_mode", "preset_mode", "expected_call"), + [ + (HVACMode.AUTO, PRESET_ECO, PRESET_ECO), + (HVACMode.AUTO, PRESET_NONE, PRESET_NONE), + (HVACMode.HEAT, PRESET_ECO, None), + ], +) +async def test_async_set_preset_mode( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + static_file: str, + hvac_mode: str, + preset_mode: str, + expected_call: str | None, +) -> None: + """Test the async_set_preset_mode function.""" + await mock_bsblan.set_static_values(static_file) + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) + + with patch( + "homeassistant.components.bsblan.climate.BSBLANClimate.hvac_mode", + new_callable=PropertyMock, + ) as mock_hvac_mode: + mock_hvac_mode.return_value = hvac_mode + + if expected_call is None: + with pytest.raises(HomeAssistantError, match="set_preset_mode_error"): + await climate_entity.async_set_preset_mode(preset_mode) + else: + with patch.object(climate_entity, "async_set_data") as mock_set_data: + await climate_entity.async_set_preset_mode(preset_mode) + mock_set_data.assert_called_once_with(preset_mode=expected_call) + + +@pytest.mark.parametrize("static_file", ["static.json"]) +async def test_async_set_temperature( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + static_file: str, +) -> None: + """Test the async_set_temperature function.""" + await mock_bsblan.set_static_values(static_file) + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) + + # Test setting temperature within the allowed range + with patch.object(climate_entity, "async_set_data") as mock_set_data: + test_temp = (climate_entity.min_temp + climate_entity.max_temp) / 2 + await climate_entity.async_set_temperature(**{ATTR_TEMPERATURE: test_temp}) + mock_set_data.assert_called_once_with(**{ATTR_TEMPERATURE: test_temp}) + + # Test setting temperature to the minimum allowed value + with patch.object(climate_entity, "async_set_data") as mock_set_data: + await climate_entity.async_set_temperature( + **{ATTR_TEMPERATURE: climate_entity.min_temp} + ) + mock_set_data.assert_called_once_with( + **{ATTR_TEMPERATURE: climate_entity.min_temp} + ) + + # Test setting temperature to the maximum allowed value + with patch.object(climate_entity, "async_set_data") as mock_set_data: + await climate_entity.async_set_temperature( + **{ATTR_TEMPERATURE: climate_entity.max_temp} + ) + mock_set_data.assert_called_once_with( + **{ATTR_TEMPERATURE: climate_entity.max_temp} + ) + + # Test setting temperature with additional parameters + with patch.object(climate_entity, "async_set_data") as mock_set_data: + test_temp = (climate_entity.min_temp + climate_entity.max_temp) / 2 + additional_param = "test_param" + await climate_entity.async_set_temperature( + **{ATTR_TEMPERATURE: test_temp, additional_param: "value"} + ) + mock_set_data.assert_called_once_with( + **{ATTR_TEMPERATURE: test_temp, additional_param: "value"} + ) + + +@pytest.mark.parametrize("static_file", ["static.json"]) +async def test_async_set_data( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + static_file: str, +) -> None: + """Test the async_set_data function.""" + await mock_bsblan.set_static_values(static_file) + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) + + # Test setting temperature + with ( + patch.object( + climate_entity.coordinator.client, "thermostat" + ) as mock_thermostat, + patch.object( + climate_entity.coordinator, "async_request_refresh" + ) as mock_refresh, + ): + await climate_entity.async_set_data(**{ATTR_TEMPERATURE: 22}) + mock_thermostat.assert_called_once_with(**{ATTR_TARGET_TEMPERATURE: 22}) + mock_refresh.assert_called_once() + + # Test setting HVAC mode + with ( + patch.object( + climate_entity.coordinator.client, "thermostat" + ) as mock_thermostat, + patch.object( + climate_entity.coordinator, "async_request_refresh" + ) as mock_refresh, + ): + await climate_entity.async_set_data(**{ATTR_HVAC_MODE: HVACMode.HEAT}) + mock_thermostat.assert_called_once_with(**{ATTR_HVAC_MODE: HVACMode.HEAT}) + mock_refresh.assert_called_once() + + # Test setting preset mode to NONE + with ( + patch.object( + climate_entity.coordinator.client, "thermostat" + ) as mock_thermostat, + patch.object( + climate_entity.coordinator, "async_request_refresh" + ) as mock_refresh, + ): + await climate_entity.async_set_data(**{ATTR_PRESET_MODE: PRESET_NONE}) + mock_thermostat.assert_called_once_with(**{ATTR_HVAC_MODE: HVACMode.AUTO}) + mock_refresh.assert_called_once() + + # Test setting preset mode to a non-NONE value + with ( + patch.object( + climate_entity.coordinator.client, "thermostat" + ) as mock_thermostat, + patch.object( + climate_entity.coordinator, "async_request_refresh" + ) as mock_refresh, + ): + await climate_entity.async_set_data(**{ATTR_PRESET_MODE: "eco"}) + mock_thermostat.assert_called_once_with(**{ATTR_HVAC_MODE: "eco"}) + mock_refresh.assert_called_once() - # Update the state in Home Assistant - await hass.helpers.entity_component.async_update_entity("climate.bsb_lan") + # Test setting multiple parameters + with ( + patch.object( + climate_entity.coordinator.client, "thermostat" + ) as mock_thermostat, + patch.object( + climate_entity.coordinator, "async_request_refresh" + ) as mock_refresh, + ): + await climate_entity.async_set_data( + **{ATTR_TEMPERATURE: 23, ATTR_HVAC_MODE: HVACMode.COOL} + ) + mock_thermostat.assert_called_once_with( + **{ATTR_TARGET_TEMPERATURE: 23, ATTR_HVAC_MODE: HVACMode.COOL} + ) + mock_refresh.assert_called_once() - # Get the state of the climate entity - state = hass.states.get("climate.bsb_lan") + # Test error handling + with ( + patch.object( + climate_entity.coordinator.client, + "thermostat", + side_effect=BSBLANError("Test error"), + ), + pytest.raises(HomeAssistantError) as exc_info, + ): + await climate_entity.async_set_data(**{ATTR_TEMPERATURE: 24}) - # Assert that the current_temperature attribute is None - assert state.attributes["current_temperature"] is None + assert "An error occurred while updating the BSBLAN device" in str(exc_info.value) + assert exc_info.value.translation_key == "set_data_error" From 15d4f39fcdcbbb02a67b083f43a97e00161ffbca Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sun, 1 Sep 2024 22:39:30 +0200 Subject: [PATCH 11/30] chore: Update BSBLAN diagnostics to handle asynchronous data retrieval --- tests/components/bsblan/test_diagnostics.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/components/bsblan/test_diagnostics.py b/tests/components/bsblan/test_diagnostics.py index 64f4adca2e4185..9b7342d21e51c1 100644 --- a/tests/components/bsblan/test_diagnostics.py +++ b/tests/components/bsblan/test_diagnostics.py @@ -1,5 +1,7 @@ """Tests for the diagnostics data provided by the BSBLan integration.""" +from unittest.mock import AsyncMock + import pytest from syrupy import SnapshotAssertion @@ -10,14 +12,18 @@ from tests.typing import ClientSessionGenerator -@pytest.mark.parametrize("mock_bsblan", ["static.json", "static_F.json"], indirect=True) +@pytest.mark.parametrize("static_file", ["static.json"]) async def test_diagnostics( hass: HomeAssistant, + mock_bsblan: AsyncMock, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, snapshot: SnapshotAssertion, + static_file: str, ) -> None: """Test diagnostics.""" + await mock_bsblan.set_static_values(static_file) + diagnostics_data = await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) From 0530fc4655586d76b59cbf040c412396bb5605ed Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Sun, 1 Sep 2024 22:41:11 +0200 Subject: [PATCH 12/30] chore: Update snapshots --- .../bsblan/snapshots/test_climate.ambr | 150 +-------------- .../bsblan/snapshots/test_diagnostics.ambr | 172 +----------------- 2 files changed, 3 insertions(+), 319 deletions(-) diff --git a/tests/components/bsblan/snapshots/test_climate.ambr b/tests/components/bsblan/snapshots/test_climate.ambr index 14a137c9133ba6..dffb7137e3674f 100644 --- a/tests/components/bsblan/snapshots/test_climate.ambr +++ b/tests/components/bsblan/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_climate_entity[climate.bsb_lan-entry] +# name: test_climate_entity_properties[static.json][climate.bsb_lan-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -44,7 +44,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_entity[climate.bsb_lan-state] +# name: test_climate_entity_properties[static.json][climate.bsb_lan-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 18.6, @@ -72,149 +72,3 @@ 'state': 'heat', }) # --- -# name: test_climate_entity[static.json][climate.bsb_lan-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': 20.0, - 'min_temp': 8.0, - 'preset_modes': list([ - 'eco', - 'none', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'climate', - 'entity_category': None, - 'entity_id': 'climate.bsb_lan', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'bsblan', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:80:41:19:69:90-climate', - 'unit_of_measurement': None, - }) -# --- -# name: test_climate_entity[static.json][climate.bsb_lan-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'current_temperature': 18.6, - 'friendly_name': 'BSB-LAN', - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': 20.0, - 'min_temp': 8.0, - 'preset_mode': 'none', - 'preset_modes': list([ - 'eco', - 'none', - ]), - 'supported_features': , - 'temperature': 18.5, - }), - 'context': , - 'entity_id': 'climate.bsb_lan', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'heat', - }) -# --- -# name: test_climate_entity[static_F.json][climate.bsb_lan-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': -6.7, - 'min_temp': -13.3, - 'preset_modes': list([ - 'eco', - 'none', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'climate', - 'entity_category': None, - 'entity_id': 'climate.bsb_lan', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'bsblan', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:80:41:19:69:90-climate', - 'unit_of_measurement': None, - }) -# --- -# name: test_climate_entity[static_F.json][climate.bsb_lan-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'current_temperature': -7.4, - 'friendly_name': 'BSB-LAN', - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': -6.7, - 'min_temp': -13.3, - 'preset_mode': 'none', - 'preset_modes': list([ - 'eco', - 'none', - ]), - 'supported_features': , - 'temperature': -7.5, - }), - 'context': , - 'entity_id': 'climate.bsb_lan', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'heat', - }) -# --- diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index 59d84bde66b62e..d540b72b524084 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_diagnostics +# name: test_diagnostics[static.json] dict({ 'coordinator_data': dict({ 'state': dict({ @@ -76,175 +76,5 @@ 'value': 'RVS21.831F/127', }), }), - 'static': dict({ - 'max_temp': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Summer/winter changeover temp heat circuit 1', - 'unit': '°C', - 'value': '20.0', - }), - 'min_temp': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Room temp frost protection setpoint', - 'unit': '°C', - 'value': '8.0', - }), - }), - }) -# --- -# name: test_diagnostics[static.json] - dict({ - 'device': dict({ - 'MAC': '00:80:41:19:69:90', - 'name': 'BSB-LAN', - 'uptime': 969402857, - 'version': '1.0.38-20200730234859', - }), - 'info': dict({ - 'controller_family': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Device family', - 'unit': '', - 'value': '211', - }), - 'controller_variant': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Device variant', - 'unit': '', - 'value': '127', - }), - 'device_identification': dict({ - 'data_type': 7, - 'desc': '', - 'name': 'Gerte-Identifikation', - 'unit': '', - 'value': 'RVS21.831F/127', - }), - }), - 'state': dict({ - 'current_temperature': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Room temp 1 actual value', - 'unit': '°C', - 'value': '18.6', - }), - 'hvac_action': dict({ - 'data_type': 1, - 'desc': 'Raumtemp’begrenzung', - 'name': 'Status heating circuit 1', - 'unit': '', - 'value': '122', - }), - 'hvac_mode': dict({ - 'data_type': 1, - 'desc': 'Komfort', - 'name': 'Operating mode', - 'unit': '', - 'value': 'heat', - }), - 'hvac_mode2': dict({ - 'data_type': 1, - 'desc': 'Reduziert', - 'name': 'Operating mode', - 'unit': '', - 'value': '2', - }), - 'room1_thermostat_mode': dict({ - 'data_type': 1, - 'desc': 'Kein Bedarf', - 'name': 'Raumthermostat 1', - 'unit': '', - 'value': '0', - }), - 'target_temperature': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Room temperature Comfort setpoint', - 'unit': '°C', - 'value': '18.5', - }), - }), - }) -# --- -# name: test_diagnostics[static_F.json] - dict({ - 'device': dict({ - 'MAC': '00:80:41:19:69:90', - 'name': 'BSB-LAN', - 'uptime': 969402857, - 'version': '1.0.38-20200730234859', - }), - 'info': dict({ - 'controller_family': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Device family', - 'unit': '', - 'value': '211', - }), - 'controller_variant': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Device variant', - 'unit': '', - 'value': '127', - }), - 'device_identification': dict({ - 'data_type': 7, - 'desc': '', - 'name': 'Gerte-Identifikation', - 'unit': '', - 'value': 'RVS21.831F/127', - }), - }), - 'state': dict({ - 'current_temperature': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Room temp 1 actual value', - 'unit': '°C', - 'value': '18.6', - }), - 'hvac_action': dict({ - 'data_type': 1, - 'desc': 'Raumtemp’begrenzung', - 'name': 'Status heating circuit 1', - 'unit': '', - 'value': '122', - }), - 'hvac_mode': dict({ - 'data_type': 1, - 'desc': 'Komfort', - 'name': 'Operating mode', - 'unit': '', - 'value': 'heat', - }), - 'hvac_mode2': dict({ - 'data_type': 1, - 'desc': 'Reduziert', - 'name': 'Operating mode', - 'unit': '', - 'value': '2', - }), - 'room1_thermostat_mode': dict({ - 'data_type': 1, - 'desc': 'Kein Bedarf', - 'name': 'Raumthermostat 1', - 'unit': '', - 'value': '0', - }), - 'target_temperature': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Room temperature Comfort setpoint', - 'unit': '°C', - 'value': '18.5', - }), - }), }) # --- From 48d92c7fa346b8e8e1ecb18579a70a28eb87fa3d Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Mon, 2 Sep 2024 08:14:06 +0200 Subject: [PATCH 13/30] Fix BSBLAN climate test for async_set_preset_mode - Update test_async_set_preset_mode to correctly handle ServiceValidationError - Check for specific translation key instead of full error message - Ensure consistency between local tests and CI environment - Import ServiceValidationError explicitly for clarity --- tests/components/bsblan/test_climate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index da0f165a699a18..fa0bea236a75e7 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -18,7 +18,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError import homeassistant.helpers.entity_registry as er from . import setup_with_selected_platforms @@ -142,8 +142,9 @@ async def test_async_set_preset_mode( mock_hvac_mode.return_value = hvac_mode if expected_call is None: - with pytest.raises(HomeAssistantError, match="set_preset_mode_error"): + with pytest.raises(ServiceValidationError) as exc_info: await climate_entity.async_set_preset_mode(preset_mode) + assert exc_info.value.translation_key == "set_preset_mode_error" else: with patch.object(climate_entity, "async_set_data") as mock_set_data: await climate_entity.async_set_preset_mode(preset_mode) From 35f4750a6f9fef5eeaea5283ad04195509c8d280 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Mon, 2 Sep 2024 20:06:40 +0200 Subject: [PATCH 14/30] Update homeassistant/components/bsblan/entity.py Co-authored-by: Joost Lekkerkerker --- homeassistant/components/bsblan/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bsblan/entity.py b/homeassistant/components/bsblan/entity.py index 9ffc8f32125e81..252c397f4f2638 100644 --- a/homeassistant/components/bsblan/entity.py +++ b/homeassistant/components/bsblan/entity.py @@ -22,7 +22,7 @@ class BSBLanEntity(CoordinatorEntity[BSBLanUpdateCoordinator]): def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None: """Initialize BSBLan entity.""" super().__init__(coordinator, data) - host = self.coordinator.config_entry.data["host"] + host = coordinator.config_entry.data["host"] mac = data.device.MAC self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mac)}, From 8069e0203d9d711067625db53ca87c993d59bec1 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 09:26:52 +0200 Subject: [PATCH 15/30] chore: Update BSBLAN conftest.py to simplify fixture and patching --- tests/components/bsblan/conftest.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index 6a319c5d252a2b..96445a4bb2349a 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -52,14 +52,9 @@ def mock_bsblan() -> Generator[MagicMock, None, None]: load_fixture("device.json", DOMAIN) ) bsblan.state.return_value = State.from_json(load_fixture("state.json", DOMAIN)) - - # Patch static values based on the test case - async def set_static_values(param): - bsblan.static_values.return_value = StaticState.from_json( - load_fixture(param, DOMAIN) - ) - - bsblan.set_static_values = set_static_values + bsblan.static_values.return_value = StaticState.from_json( + load_fixture("static.json", DOMAIN) + ) yield bsblan From c3dcd11c8b02f08d7f8b56b0f1031876b1f3f95d Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 09:27:33 +0200 Subject: [PATCH 16/30] chore: Update BSBLAN integration setup function parameter name --- tests/components/bsblan/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py index 239e6ca7b3521b..3892fcaaaca1a9 100644 --- a/tests/components/bsblan/__init__.py +++ b/tests/components/bsblan/__init__.py @@ -9,10 +9,10 @@ async def setup_with_selected_platforms( - hass: HomeAssistant, entry: MockConfigEntry, platforms: list[Platform] + hass: HomeAssistant, config_entry: MockConfigEntry, platforms: list[Platform] ) -> None: """Set up the BSBLAN integration with the selected platforms.""" - entry.add_to_hass(hass) + config_entry.add_to_hass(hass) with patch("homeassistant.components.bsblan.PLATFORMS", platforms): - assert await hass.config_entries.async_setup(entry.entry_id) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From c2b97fbdb6e1ed064172c097c60e235841bfce42 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 09:29:15 +0200 Subject: [PATCH 17/30] chore: removed set_static_value --- tests/components/bsblan/test_diagnostics.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/components/bsblan/test_diagnostics.py b/tests/components/bsblan/test_diagnostics.py index 9b7342d21e51c1..aea53f8a1a2c60 100644 --- a/tests/components/bsblan/test_diagnostics.py +++ b/tests/components/bsblan/test_diagnostics.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock -import pytest from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -12,17 +11,14 @@ from tests.typing import ClientSessionGenerator -@pytest.mark.parametrize("static_file", ["static.json"]) async def test_diagnostics( hass: HomeAssistant, mock_bsblan: AsyncMock, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, snapshot: SnapshotAssertion, - static_file: str, ) -> None: """Test diagnostics.""" - await mock_bsblan.set_static_values(static_file) diagnostics_data = await get_diagnostics_for_config_entry( hass, hass_client, init_integration From 0dade590ea720c86c0ebbbd2737e836938e9df77 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 13:56:06 +0200 Subject: [PATCH 18/30] refactor: Improve BSBLANClimate async_set_preset_mode method This commit refactors the async_set_preset_mode method in the BSBLANClimate class to improve code readability and maintainability. The method now checks if the HVAC mode is not set to AUTO and the preset mode is not NONE before raising a ServiceValidationError. Co-authored-by: Joost Lekkerkerker --- homeassistant/components/bsblan/climate.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index ae7116143df9c3..bd1b8c74131646 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -126,15 +126,13 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" - # only allow preset mode when hvac mode is auto - if self.hvac_mode == HVACMode.AUTO: - await self.async_set_data(preset_mode=preset_mode) - else: + if self.hvac_mode != HVACMode.AUTO and preset_mode != PRESET_NONE: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="set_preset_mode_error", translation_placeholders={"preset_mode": preset_mode}, ) + await self.async_set_data(preset_mode=preset_mode) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" @@ -148,11 +146,14 @@ async def async_set_data(self, **kwargs: Any) -> None: if ATTR_HVAC_MODE in kwargs: data[ATTR_HVAC_MODE] = kwargs[ATTR_HVAC_MODE] if ATTR_PRESET_MODE in kwargs: - # If preset mode is None, set hvac to auto - if kwargs[ATTR_PRESET_MODE] == PRESET_NONE: - data[ATTR_HVAC_MODE] = HVACMode.AUTO + if kwargs[ATTR_PRESET_MODE] == PRESET_ECO: + data[ATTR_HVAC_MODE] = PRESET_ECO + elif kwargs[ATTR_PRESET_MODE] == PRESET_NONE: + # Don't change the HVAC mode when setting to PRESET_NONE + pass else: - data[ATTR_HVAC_MODE] = kwargs[ATTR_PRESET_MODE] + data[ATTR_HVAC_MODE] = HVACMode.AUTO + try: await self.coordinator.client.thermostat(**data) except BSBLANError as err: From 6c6982204b948326151da941203293e9cf05f7a7 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 14:25:27 +0200 Subject: [PATCH 19/30] refactor: Improve tests test_celsius_fahrenheit test_climate_entity_properties test_async_set_hvac_mode test_async_set_preset_mode still broken. Not sure why hvac mode will not set. THis causes error with preset mode set --- tests/components/bsblan/test_climate.py | 256 ++++++++++++++++-------- 1 file changed, 170 insertions(+), 86 deletions(-) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index fa0bea236a75e7..e1d2a79b0f47af 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -1,12 +1,14 @@ """Tests for the BSB-Lan climate platform.""" -from unittest.mock import AsyncMock, PropertyMock, patch +from datetime import timedelta +import json +from unittest.mock import AsyncMock, MagicMock, patch -from bsblan import BSBLANError +from bsblan import BSBLANError, StaticState +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.bsblan.climate import BSBLANClimate from homeassistant.components.bsblan.const import ATTR_TARGET_TEMPERATURE from homeassistant.components.climate import ( ATTR_HVAC_MODE, @@ -14,109 +16,162 @@ DOMAIN as CLIMATE_DOMAIN, PRESET_ECO, PRESET_NONE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, HVACMode, ) -from homeassistant.const import ATTR_TEMPERATURE, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + Platform, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.entity_registry as er from . import setup_with_selected_platforms -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) ENTITY_ID = "climate.bsb_lan" -@pytest.mark.parametrize("static_file", ["static.json"]) -async def test_climate_entity_properties( +@pytest.mark.parametrize( + ("static_file", "temperature_unit"), + [ + ("static.json", UnitOfTemperature.CELSIUS), + ("static_F.json", UnitOfTemperature.FAHRENHEIT), + ], +) +async def test_celsius_fahrenheit( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, static_file: str, + temperature_unit: str, ) -> None: - """Test the initial parameters in Celsius.""" - await mock_bsblan.set_static_values(static_file) + """Test Celsius and Fahrenheit temperature units.""" + # Load static data from fixture + static_data = json.loads(load_fixture(static_file, CLIMATE_DOMAIN)) + + # Patch the static_values method to return our test data + with patch.object( + mock_bsblan, "static_values", return_value=StaticState.from_dict(static_data) + ): + # Set up the climate platform + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + # Take a snapshot of the entity registry + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +async def test_climate_entity_properties( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the climate entity properties.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) - # Test when current_temperature is "---" - with patch.object( - mock_bsblan.state.return_value.current_temperature, "value", "---" - ): - await hass.helpers.entity_component.async_update_entity(ENTITY_ID) - state = hass.states.get(ENTITY_ID) - assert state.attributes["current_temperature"] is None + mock_current_temp = MagicMock() + mock_current_temp.value = "---" + mock_bsblan.state.return_value.current_temperature = mock_current_temp + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes["current_temperature"] is None # Test target_temperature - target_temperature = 16.2 - with patch.object( - mock_bsblan.state.return_value.target_temperature, "value", target_temperature - ): - await hass.helpers.entity_component.async_update_entity(ENTITY_ID) - state = hass.states.get(ENTITY_ID) - assert state.attributes["temperature"] == target_temperature + mock_target_temp = MagicMock() + mock_target_temp.value = "23.5" + mock_bsblan.state.return_value.target_temperature = mock_target_temp + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - # Test hvac_mode when preset_mode is ECO - with patch.object(mock_bsblan.state.return_value.hvac_mode, "value", PRESET_ECO): - assert climate_entity.hvac_mode == HVACMode.AUTO + state = hass.states.get(ENTITY_ID) + assert state.attributes["temperature"] == 23.5 - # Test hvac_mode with other values - with patch.object(mock_bsblan.state.return_value.hvac_mode, "value", HVACMode.HEAT): - assert climate_entity.hvac_mode == HVACMode.HEAT + # Test hvac_mode + mock_hvac_mode = MagicMock() + mock_hvac_mode.value = HVACMode.AUTO + mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.AUTO # Test preset_mode - with patch.object( - BSBLANClimate, "hvac_mode", new_callable=PropertyMock - ) as mock_hvac_mode: - mock_hvac_mode.return_value = HVACMode.AUTO - mock_bsblan.state.return_value.hvac_mode.value = PRESET_ECO - await hass.helpers.entity_component.async_update_entity(ENTITY_ID) - state = hass.states.get(ENTITY_ID) - assert state.attributes["preset_mode"] == PRESET_ECO + mock_hvac_mode.value = PRESET_ECO + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes["preset_mode"] == PRESET_ECO @pytest.mark.parametrize("static_file", ["static.json"]) @pytest.mark.parametrize( - ("mode", "expected_call"), - [ - (HVACMode.HEAT, HVACMode.HEAT), - (HVACMode.COOL, HVACMode.COOL), - (HVACMode.AUTO, HVACMode.AUTO), - (HVACMode.OFF, HVACMode.OFF), - ], + "mode", + [HVACMode.HEAT, HVACMode.AUTO, HVACMode.OFF], ) async def test_async_set_hvac_mode( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, static_file: str, - mode: str, - expected_call: str, + mode: HVACMode, ) -> None: - """Test the async_set_hvac_mode function.""" - await mock_bsblan.set_static_values(static_file) - await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + """Test setting HVAC mode via service call.""" + static_data = json.loads(load_fixture(static_file, CLIMATE_DOMAIN)) + with patch.object( + mock_bsblan, "static_values", return_value=StaticState.from_dict(static_data) + ): + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) - climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) + # Call the service to set HVAC mode + await hass.services.async_call( + domain=CLIMATE_DOMAIN, + service=SERVICE_SET_HVAC_MODE, + service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: mode}, + blocking=True, + ) - with patch.object(climate_entity, "async_set_data") as mock_set_data: - await climate_entity.async_set_hvac_mode(mode) - mock_set_data.assert_called_once_with(hvac_mode=expected_call) + # Assert that the thermostat method was called + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=mode) + mock_bsblan.thermostat.reset_mock() -@pytest.mark.parametrize("static_file", ["static.json"]) @pytest.mark.parametrize( - ("hvac_mode", "preset_mode", "expected_call"), + ("hvac_mode", "preset_mode", "expected_success"), [ - (HVACMode.AUTO, PRESET_ECO, PRESET_ECO), - (HVACMode.AUTO, PRESET_NONE, PRESET_NONE), - (HVACMode.HEAT, PRESET_ECO, None), + (HVACMode.AUTO, PRESET_ECO, True), + (HVACMode.AUTO, PRESET_NONE, True), + (HVACMode.HEAT, PRESET_ECO, False), ], ) async def test_async_set_preset_mode( @@ -124,43 +179,75 @@ async def test_async_set_preset_mode( mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, - static_file: str, - hvac_mode: str, + hvac_mode: HVACMode, preset_mode: str, - expected_call: str | None, + expected_success: bool, + freezer: FrozenDateTimeFactory, ) -> None: - """Test the async_set_preset_mode function.""" - await mock_bsblan.set_static_values(static_file) + """Test setting preset mode via service call.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) - climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) + # Set the HVAC mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: hvac_mode}, + blocking=True, + ) + # wait for the service call to complete + await hass.async_block_till_done() + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Verify HVAC mode was set correctly + # state = hass.states.get(ENTITY_ID) + # assert state.state == hvac_mode, f"HVAC mode not set correctly. Expected {hvac_mode}, got {state.state}" + + # Reset the mock to clear the call from setting HVAC mode + # mock_bsblan.thermostat.reset_mock() + + # Attempt to set the preset mode + if not expected_success: + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset_mode}, + blocking=True, + ) + assert "set_preset_mode_error" in str(exc_info.value) + else: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset_mode}, + blocking=True, + ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.bsblan.climate.BSBLANClimate.hvac_mode", - new_callable=PropertyMock, - ) as mock_hvac_mode: - mock_hvac_mode.return_value = hvac_mode + expected_hvac_mode = PRESET_ECO if preset_mode == PRESET_ECO else HVACMode.AUTO + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=expected_hvac_mode) - if expected_call is None: - with pytest.raises(ServiceValidationError) as exc_info: - await climate_entity.async_set_preset_mode(preset_mode) - assert exc_info.value.translation_key == "set_preset_mode_error" - else: - with patch.object(climate_entity, "async_set_data") as mock_set_data: - await climate_entity.async_set_preset_mode(preset_mode) - mock_set_data.assert_called_once_with(preset_mode=expected_call) + # Verify the final state + state = hass.states.get(ENTITY_ID) + assert state.state == hvac_mode + assert state.attributes.get("preset_mode") == ( + preset_mode if expected_success else PRESET_NONE + ) + + # Reset the mock for the next iteration + mock_bsblan.thermostat.reset_mock() -@pytest.mark.parametrize("static_file", ["static.json"]) async def test_async_set_temperature( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, - static_file: str, ) -> None: """Test the async_set_temperature function.""" - await mock_bsblan.set_static_values(static_file) await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) @@ -201,16 +288,13 @@ async def test_async_set_temperature( ) -@pytest.mark.parametrize("static_file", ["static.json"]) async def test_async_set_data( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, - static_file: str, ) -> None: """Test the async_set_data function.""" - await mock_bsblan.set_static_values(static_file) await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) From deba43387e2e0cdd290752a1d19ed2118accb0e7 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 14:26:20 +0200 Subject: [PATCH 20/30] update snapshot --- .../bsblan/snapshots/test_climate.ambr | 292 ++++++++++++++++++ .../bsblan/snapshots/test_diagnostics.ambr | 2 +- 2 files changed, 293 insertions(+), 1 deletion(-) diff --git a/tests/components/bsblan/snapshots/test_climate.ambr b/tests/components/bsblan/snapshots/test_climate.ambr index dffb7137e3674f..a9c3987214c393 100644 --- a/tests/components/bsblan/snapshots/test_climate.ambr +++ b/tests/components/bsblan/snapshots/test_climate.ambr @@ -1,4 +1,223 @@ # serializer version: 1 +# name: test_celsius_fahrenheit[static.json-\xb0C][climate.bsb_lan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 20.0, + 'min_temp': 8.0, + 'preset_modes': list([ + 'eco', + 'none', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.bsb_lan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:80:41:19:69:90-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_celsius_fahrenheit[static.json-\xb0C][climate.bsb_lan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 18.6, + 'friendly_name': 'BSB-LAN', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 20.0, + 'min_temp': 8.0, + 'preset_mode': 'none', + 'preset_modes': list([ + 'eco', + 'none', + ]), + 'supported_features': , + 'temperature': 18.5, + }), + 'context': , + 'entity_id': 'climate.bsb_lan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_celsius_fahrenheit[static_F.json-\xb0F][climate.bsb_lan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': -6.7, + 'min_temp': -13.3, + 'preset_modes': list([ + 'eco', + 'none', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.bsb_lan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:80:41:19:69:90-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_celsius_fahrenheit[static_F.json-\xb0F][climate.bsb_lan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -7.4, + 'friendly_name': 'BSB-LAN', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': -6.7, + 'min_temp': -13.3, + 'preset_mode': 'none', + 'preset_modes': list([ + 'eco', + 'none', + ]), + 'supported_features': , + 'temperature': -7.5, + }), + 'context': , + 'entity_id': 'climate.bsb_lan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_climate_entity_properties[climate.bsb_lan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 20.0, + 'min_temp': 8.0, + 'preset_modes': list([ + 'eco', + 'none', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.bsb_lan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:80:41:19:69:90-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_entity_properties[climate.bsb_lan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 18.6, + 'friendly_name': 'BSB-LAN', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 20.0, + 'min_temp': 8.0, + 'preset_mode': 'none', + 'preset_modes': list([ + 'eco', + 'none', + ]), + 'supported_features': , + 'temperature': 18.5, + }), + 'context': , + 'entity_id': 'climate.bsb_lan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- # name: test_climate_entity_properties[static.json][climate.bsb_lan-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -72,3 +291,76 @@ 'state': 'heat', }) # --- +# name: test_climate_entity_properties[static_f.json][climate.bsb_lan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': -6.7, + 'min_temp': -13.3, + 'preset_modes': list([ + 'eco', + 'none', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.bsb_lan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bsblan', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '00:80:41:19:69:90-climate', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_entity_properties[static_f.json][climate.bsb_lan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -7.4, + 'friendly_name': 'BSB-LAN', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': -6.7, + 'min_temp': -13.3, + 'preset_mode': 'none', + 'preset_modes': list([ + 'eco', + 'none', + ]), + 'supported_features': , + 'temperature': -7.5, + }), + 'context': , + 'entity_id': 'climate.bsb_lan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index d540b72b524084..59899b96196cee 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_diagnostics[static.json] +# name: test_diagnostics dict({ 'coordinator_data': dict({ 'state': dict({ From ef2eba9ddf573c5329d2d72972692ea51e72ca5f Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 14:43:24 +0200 Subject: [PATCH 21/30] fix DOMAIN bsblan --- tests/components/bsblan/test_climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index e1d2a79b0f47af..76a538bff17deb 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -9,7 +9,7 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.bsblan.const import ATTR_TARGET_TEMPERATURE +from homeassistant.components.bsblan.const import ATTR_TARGET_TEMPERATURE, DOMAIN from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, @@ -60,7 +60,7 @@ async def test_celsius_fahrenheit( ) -> None: """Test Celsius and Fahrenheit temperature units.""" # Load static data from fixture - static_data = json.loads(load_fixture(static_file, CLIMATE_DOMAIN)) + static_data = json.loads(load_fixture(static_file, DOMAIN)) # Patch the static_values method to return our test data with patch.object( @@ -147,7 +147,7 @@ async def test_async_set_hvac_mode( mode: HVACMode, ) -> None: """Test setting HVAC mode via service call.""" - static_data = json.loads(load_fixture(static_file, CLIMATE_DOMAIN)) + static_data = json.loads(load_fixture(static_file, DOMAIN)) with patch.object( mock_bsblan, "static_values", return_value=StaticState.from_dict(static_data) ): From 1104acfec421ba430fddd2ab20c53e04642c927e Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 16:23:01 +0200 Subject: [PATCH 22/30] refactor: Improve BSBLANClimate async_set_data method --- homeassistant/components/bsblan/climate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index bd1b8c74131646..cf2691519359e4 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -149,10 +149,7 @@ async def async_set_data(self, **kwargs: Any) -> None: if kwargs[ATTR_PRESET_MODE] == PRESET_ECO: data[ATTR_HVAC_MODE] = PRESET_ECO elif kwargs[ATTR_PRESET_MODE] == PRESET_NONE: - # Don't change the HVAC mode when setting to PRESET_NONE - pass - else: - data[ATTR_HVAC_MODE] = HVACMode.AUTO + data[ATTR_HVAC_MODE] = PRESET_NONE try: await self.coordinator.client.thermostat(**data) From bfcf6c715868c466293994b11cb53b1a3ef5d598 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 16:23:48 +0200 Subject: [PATCH 23/30] refactor: fix last tests --- tests/components/bsblan/test_climate.py | 233 +++++++++--------------- 1 file changed, 88 insertions(+), 145 deletions(-) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index 76a538bff17deb..cda7d95f0563cf 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -9,7 +9,7 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.bsblan.const import ATTR_TARGET_TEMPERATURE, DOMAIN +from homeassistant.components.bsblan.const import DOMAIN from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, @@ -18,6 +18,7 @@ PRESET_NONE, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, HVACMode, ) from homeassistant.const import ( @@ -27,7 +28,7 @@ UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError import homeassistant.helpers.entity_registry as er from . import setup_with_selected_platforms @@ -187,26 +188,10 @@ async def test_async_set_preset_mode( """Test setting preset mode via service call.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) - # Set the HVAC mode - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: hvac_mode}, - blocking=True, - ) - # wait for the service call to complete - await hass.async_block_till_done() - - freezer.tick(timedelta(minutes=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - # Verify HVAC mode was set correctly - # state = hass.states.get(ENTITY_ID) - # assert state.state == hvac_mode, f"HVAC mode not set correctly. Expected {hvac_mode}, got {state.state}" - - # Reset the mock to clear the call from setting HVAC mode - # mock_bsblan.thermostat.reset_mock() + # patch hvac_mode + mock_hvac_mode = MagicMock() + mock_hvac_mode.value = hvac_mode + mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode # Attempt to set the preset mode if not expected_success: @@ -227,157 +212,115 @@ async def test_async_set_preset_mode( ) await hass.async_block_till_done() - expected_hvac_mode = PRESET_ECO if preset_mode == PRESET_ECO else HVACMode.AUTO - mock_bsblan.thermostat.assert_called_once_with(hvac_mode=expected_hvac_mode) - - # Verify the final state - state = hass.states.get(ENTITY_ID) - assert state.state == hvac_mode - assert state.attributes.get("preset_mode") == ( - preset_mode if expected_success else PRESET_NONE - ) - # Reset the mock for the next iteration mock_bsblan.thermostat.reset_mock() +@pytest.mark.parametrize( + ("target_temp", "expected_result"), + [ + (8.0, "success"), # Min temperature + (15.0, "success"), # Mid-range temperature + (20.0, "success"), # Max temperature + (7.9, "failure"), # Just below min + (20.1, "failure"), # Just above max + ], +) async def test_async_set_temperature( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, + target_temp: float, + expected_result: str, ) -> None: - """Test the async_set_temperature function.""" + """Test setting temperature via service call.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) - climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) - - # Test setting temperature within the allowed range - with patch.object(climate_entity, "async_set_data") as mock_set_data: - test_temp = (climate_entity.min_temp + climate_entity.max_temp) / 2 - await climate_entity.async_set_temperature(**{ATTR_TEMPERATURE: test_temp}) - mock_set_data.assert_called_once_with(**{ATTR_TEMPERATURE: test_temp}) - - # Test setting temperature to the minimum allowed value - with patch.object(climate_entity, "async_set_data") as mock_set_data: - await climate_entity.async_set_temperature( - **{ATTR_TEMPERATURE: climate_entity.min_temp} - ) - mock_set_data.assert_called_once_with( - **{ATTR_TEMPERATURE: climate_entity.min_temp} - ) - - # Test setting temperature to the maximum allowed value - with patch.object(climate_entity, "async_set_data") as mock_set_data: - await climate_entity.async_set_temperature( - **{ATTR_TEMPERATURE: climate_entity.max_temp} - ) - mock_set_data.assert_called_once_with( - **{ATTR_TEMPERATURE: climate_entity.max_temp} + if expected_result == "success": + # Call the service to set temperature + await hass.services.async_call( + domain=CLIMATE_DOMAIN, + service=SERVICE_SET_TEMPERATURE, + service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: target_temp}, + blocking=True, ) + # Assert that the thermostat method was called with the correct temperature + mock_bsblan.thermostat.assert_called_once_with(target_temperature=target_temp) + else: + # Expect a ServiceValidationError for temperatures out of range + with pytest.raises(ServiceValidationError) as exc_info: + await hass.services.async_call( + domain=CLIMATE_DOMAIN, + service=SERVICE_SET_TEMPERATURE, + service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: target_temp}, + blocking=True, + ) + assert exc_info.value.translation_key == "temp_out_of_range" - # Test setting temperature with additional parameters - with patch.object(climate_entity, "async_set_data") as mock_set_data: - test_temp = (climate_entity.min_temp + climate_entity.max_temp) / 2 - additional_param = "test_param" - await climate_entity.async_set_temperature( - **{ATTR_TEMPERATURE: test_temp, additional_param: "value"} - ) - mock_set_data.assert_called_once_with( - **{ATTR_TEMPERATURE: test_temp, additional_param: "value"} - ) + mock_bsblan.thermostat.reset_mock() async def test_async_set_data( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, ) -> None: - """Test the async_set_data function.""" + """Test setting data via service calls.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) - climate_entity = hass.data[CLIMATE_DOMAIN].get_entity(ENTITY_ID) - # Test setting temperature - with ( - patch.object( - climate_entity.coordinator.client, "thermostat" - ) as mock_thermostat, - patch.object( - climate_entity.coordinator, "async_request_refresh" - ) as mock_refresh, - ): - await climate_entity.async_set_data(**{ATTR_TEMPERATURE: 22}) - mock_thermostat.assert_called_once_with(**{ATTR_TARGET_TEMPERATURE: 22}) - mock_refresh.assert_called_once() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 19}, + blocking=True, + ) + mock_bsblan.thermostat.assert_called_once_with(target_temperature=19) + mock_bsblan.thermostat.reset_mock() # Test setting HVAC mode - with ( - patch.object( - climate_entity.coordinator.client, "thermostat" - ) as mock_thermostat, - patch.object( - climate_entity.coordinator, "async_request_refresh" - ) as mock_refresh, - ): - await climate_entity.async_set_data(**{ATTR_HVAC_MODE: HVACMode.HEAT}) - mock_thermostat.assert_called_once_with(**{ATTR_HVAC_MODE: HVACMode.HEAT}) - mock_refresh.assert_called_once() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=HVACMode.HEAT) + mock_bsblan.thermostat.reset_mock() + + # Patch HVAC mode to AUTO + mock_hvac_mode = MagicMock() + mock_hvac_mode.value = HVACMode.AUTO + mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode + + # Test setting preset mode to ECO + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_ECO}, + blocking=True, + ) + mock_bsblan.thermostat.assert_called_once_with(hvac_mode=PRESET_ECO) + mock_bsblan.thermostat.reset_mock() # Test setting preset mode to NONE - with ( - patch.object( - climate_entity.coordinator.client, "thermostat" - ) as mock_thermostat, - patch.object( - climate_entity.coordinator, "async_request_refresh" - ) as mock_refresh, - ): - await climate_entity.async_set_data(**{ATTR_PRESET_MODE: PRESET_NONE}) - mock_thermostat.assert_called_once_with(**{ATTR_HVAC_MODE: HVACMode.AUTO}) - mock_refresh.assert_called_once() - - # Test setting preset mode to a non-NONE value - with ( - patch.object( - climate_entity.coordinator.client, "thermostat" - ) as mock_thermostat, - patch.object( - climate_entity.coordinator, "async_request_refresh" - ) as mock_refresh, - ): - await climate_entity.async_set_data(**{ATTR_PRESET_MODE: "eco"}) - mock_thermostat.assert_called_once_with(**{ATTR_HVAC_MODE: "eco"}) - mock_refresh.assert_called_once() - - # Test setting multiple parameters - with ( - patch.object( - climate_entity.coordinator.client, "thermostat" - ) as mock_thermostat, - patch.object( - climate_entity.coordinator, "async_request_refresh" - ) as mock_refresh, - ): - await climate_entity.async_set_data( - **{ATTR_TEMPERATURE: 23, ATTR_HVAC_MODE: HVACMode.COOL} - ) - mock_thermostat.assert_called_once_with( - **{ATTR_TARGET_TEMPERATURE: 23, ATTR_HVAC_MODE: HVACMode.COOL} - ) - mock_refresh.assert_called_once() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + mock_bsblan.thermostat.assert_called_once() + mock_bsblan.thermostat.reset_mock() # Test error handling - with ( - patch.object( - climate_entity.coordinator.client, - "thermostat", - side_effect=BSBLANError("Test error"), - ), - pytest.raises(HomeAssistantError) as exc_info, - ): - await climate_entity.async_set_data(**{ATTR_TEMPERATURE: 24}) - + mock_bsblan.thermostat.side_effect = BSBLANError("Test error") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 20}, + blocking=True, + ) assert "An error occurred while updating the BSBLAN device" in str(exc_info.value) assert exc_info.value.translation_key == "set_data_error" From 6b3b4e224b908b76ef84955e9e55362ac35902f6 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 16:31:32 +0200 Subject: [PATCH 24/30] refactor: Simplify async_get_config_entry_diagnostics method --- homeassistant/components/bsblan/diagnostics.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py index 1023fdf97e715e..8d49e4a065462b 100644 --- a/homeassistant/components/bsblan/diagnostics.py +++ b/homeassistant/components/bsblan/diagnostics.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from typing import Any from homeassistant.config_entries import ConfigEntry @@ -19,15 +18,9 @@ async def async_get_config_entry_diagnostics( data: BSBLanData = hass.data[DOMAIN][entry.entry_id] return { - "info": await data.info.to_dict() - if asyncio.iscoroutinefunction(data.info.to_dict) - else data.info.to_dict(), - "device": await data.device.to_dict() - if asyncio.iscoroutinefunction(data.device.to_dict) - else data.device.to_dict(), + "info": data.info.to_dict(), + "device": data.device.to_dict(), "coordinator_data": { - "state": await data.coordinator.data.state.to_dict() - if asyncio.iscoroutinefunction(data.coordinator.data.state.to_dict) - else data.coordinator.data.state.to_dict(), + "state": data.coordinator.data.state.to_dict(), }, } From 41aae4e49b17221ae59359c0e894daf1f85cd5cb Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 16:48:42 +0200 Subject: [PATCH 25/30] refactor: Improve BSBLANClimate async_set_temperature method This commit improves the async_set_temperature method in the BSBLANClimate class. It removes the unnecessary parameter "expected_result" and simplifies the code by directly calling the service to set the temperature. The method now correctly asserts that the thermostat method is called with the correct temperature. --- tests/components/bsblan/test_climate.py | 41 ++++++++----------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index cda7d95f0563cf..b6bbe671996e21 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -28,7 +28,7 @@ UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.entity_registry as er from . import setup_with_selected_platforms @@ -217,13 +217,11 @@ async def test_async_set_preset_mode( @pytest.mark.parametrize( - ("target_temp", "expected_result"), + ("target_temp"), [ - (8.0, "success"), # Min temperature - (15.0, "success"), # Mid-range temperature - (20.0, "success"), # Max temperature - (7.9, "failure"), # Just below min - (20.1, "failure"), # Just above max + (8.0), # Min temperature + (15.0), # Mid-range temperature + (20.0), # Max temperature ], ) async def test_async_set_temperature( @@ -231,31 +229,18 @@ async def test_async_set_temperature( mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, target_temp: float, - expected_result: str, ) -> None: """Test setting temperature via service call.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) - if expected_result == "success": - # Call the service to set temperature - await hass.services.async_call( - domain=CLIMATE_DOMAIN, - service=SERVICE_SET_TEMPERATURE, - service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: target_temp}, - blocking=True, - ) - # Assert that the thermostat method was called with the correct temperature - mock_bsblan.thermostat.assert_called_once_with(target_temperature=target_temp) - else: - # Expect a ServiceValidationError for temperatures out of range - with pytest.raises(ServiceValidationError) as exc_info: - await hass.services.async_call( - domain=CLIMATE_DOMAIN, - service=SERVICE_SET_TEMPERATURE, - service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: target_temp}, - blocking=True, - ) - assert exc_info.value.translation_key == "temp_out_of_range" + await hass.services.async_call( + domain=CLIMATE_DOMAIN, + service=SERVICE_SET_TEMPERATURE, + service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: target_temp}, + blocking=True, + ) + # Assert that the thermostat method was called with the correct temperature + mock_bsblan.thermostat.assert_called_once_with(target_temperature=target_temp) mock_bsblan.thermostat.reset_mock() From 14a3266e8783c801266c30c57c23fb6e5686430c Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 16:51:47 +0200 Subject: [PATCH 26/30] refactor: Add static data to async_get_config_entry_diagnostics --- homeassistant/components/bsblan/diagnostics.py | 1 + .../bsblan/snapshots/test_diagnostics.ambr | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py index 8d49e4a065462b..27c2f14fe71505 100644 --- a/homeassistant/components/bsblan/diagnostics.py +++ b/homeassistant/components/bsblan/diagnostics.py @@ -22,5 +22,6 @@ async def async_get_config_entry_diagnostics( "device": data.device.to_dict(), "coordinator_data": { "state": data.coordinator.data.state.to_dict(), + "static": data.static.to_dict(), }, } diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index 59899b96196cee..964c45e7b65cf6 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -46,6 +46,22 @@ 'value': '18.5', }), }), + 'static': dict({ + 'max_temp': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Summer/winter changeover temp heat circuit 1', + 'unit': '°C', + 'value': '20.0', + }), + 'min_temp': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temp frost protection setpoint', + 'unit': '°C', + 'value': '8.0', + }), + }), }), 'device': dict({ 'MAC': '00:80:41:19:69:90', From 2d2704c09415a3350dc58cb682652c9bab7c0b94 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 16:52:51 +0200 Subject: [PATCH 27/30] refactor: Add static data to async_get_config_entry_diagnostics right place --- .../components/bsblan/diagnostics.py | 2 +- .../bsblan/snapshots/test_diagnostics.ambr | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py index 27c2f14fe71505..b4ff67f4fbfe97 100644 --- a/homeassistant/components/bsblan/diagnostics.py +++ b/homeassistant/components/bsblan/diagnostics.py @@ -22,6 +22,6 @@ async def async_get_config_entry_diagnostics( "device": data.device.to_dict(), "coordinator_data": { "state": data.coordinator.data.state.to_dict(), - "static": data.static.to_dict(), }, + "static": data.static.to_dict(), } diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index 964c45e7b65cf6..c9a82edf4e2f86 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -46,22 +46,6 @@ 'value': '18.5', }), }), - 'static': dict({ - 'max_temp': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Summer/winter changeover temp heat circuit 1', - 'unit': '°C', - 'value': '20.0', - }), - 'min_temp': dict({ - 'data_type': 0, - 'desc': '', - 'name': 'Room temp frost protection setpoint', - 'unit': '°C', - 'value': '8.0', - }), - }), }), 'device': dict({ 'MAC': '00:80:41:19:69:90', @@ -92,5 +76,21 @@ 'value': 'RVS21.831F/127', }), }), + 'static': dict({ + 'max_temp': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Summer/winter changeover temp heat circuit 1', + 'unit': '°C', + 'value': '20.0', + }), + 'min_temp': dict({ + 'data_type': 0, + 'desc': '', + 'name': 'Room temp frost protection setpoint', + 'unit': '°C', + 'value': '8.0', + }), + }), }) # --- From c6b9a3a3edb876c5ab84425528b987e2c53af5cd Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 21:56:25 +0200 Subject: [PATCH 28/30] refactor: Improve error message for setting preset mode This commit updates the error message in the BSBLANClimate class when trying to set the preset mode. --- homeassistant/components/bsblan/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index cf2691519359e4..3a204a9e0c244f 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -128,6 +128,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" if self.hvac_mode != HVACMode.AUTO and preset_mode != PRESET_NONE: raise ServiceValidationError( + "Preset mode can only be set when HVAC mode is set to 'auto'", translation_domain=DOMAIN, translation_key="set_preset_mode_error", translation_placeholders={"preset_mode": preset_mode}, From 589d2bb78c32e9cb9f3b2009211319b3c25636d6 Mon Sep 17 00:00:00 2001 From: "Willem-Jan L. van Rootselaar" Date: Fri, 6 Sep 2024 21:58:44 +0200 Subject: [PATCH 29/30] refactor: Improve tests --- .../bsblan/snapshots/test_climate.ambr | 154 +----------------- tests/components/bsblan/test_climate.py | 91 ++++++----- 2 files changed, 51 insertions(+), 194 deletions(-) diff --git a/tests/components/bsblan/snapshots/test_climate.ambr b/tests/components/bsblan/snapshots/test_climate.ambr index a9c3987214c393..4eb70fe2658152 100644 --- a/tests/components/bsblan/snapshots/test_climate.ambr +++ b/tests/components/bsblan/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_celsius_fahrenheit[static.json-\xb0C][climate.bsb_lan-entry] +# name: test_celsius_fahrenheit[static.json][climate.bsb_lan-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -44,7 +44,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_celsius_fahrenheit[static.json-\xb0C][climate.bsb_lan-state] +# name: test_celsius_fahrenheit[static.json][climate.bsb_lan-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 18.6, @@ -72,7 +72,7 @@ 'state': 'heat', }) # --- -# name: test_celsius_fahrenheit[static_F.json-\xb0F][climate.bsb_lan-entry] +# name: test_celsius_fahrenheit[static_F.json][climate.bsb_lan-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -117,7 +117,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_celsius_fahrenheit[static_F.json-\xb0F][climate.bsb_lan-state] +# name: test_celsius_fahrenheit[static_F.json][climate.bsb_lan-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -7.4, @@ -218,149 +218,3 @@ 'state': 'heat', }) # --- -# name: test_climate_entity_properties[static.json][climate.bsb_lan-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': 20.0, - 'min_temp': 8.0, - 'preset_modes': list([ - 'eco', - 'none', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'climate', - 'entity_category': None, - 'entity_id': 'climate.bsb_lan', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'bsblan', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:80:41:19:69:90-climate', - 'unit_of_measurement': None, - }) -# --- -# name: test_climate_entity_properties[static.json][climate.bsb_lan-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'current_temperature': 18.6, - 'friendly_name': 'BSB-LAN', - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': 20.0, - 'min_temp': 8.0, - 'preset_mode': 'none', - 'preset_modes': list([ - 'eco', - 'none', - ]), - 'supported_features': , - 'temperature': 18.5, - }), - 'context': , - 'entity_id': 'climate.bsb_lan', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'heat', - }) -# --- -# name: test_climate_entity_properties[static_f.json][climate.bsb_lan-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': -6.7, - 'min_temp': -13.3, - 'preset_modes': list([ - 'eco', - 'none', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'climate', - 'entity_category': None, - 'entity_id': 'climate.bsb_lan', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'bsblan', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00:80:41:19:69:90-climate', - 'unit_of_measurement': None, - }) -# --- -# name: test_climate_entity_properties[static_f.json][climate.bsb_lan-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'current_temperature': -7.4, - 'friendly_name': 'BSB-LAN', - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': -6.7, - 'min_temp': -13.3, - 'preset_mode': 'none', - 'preset_modes': list([ - 'eco', - 'none', - ]), - 'supported_features': , - 'temperature': -7.5, - }), - 'context': , - 'entity_id': 'climate.bsb_lan', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'heat', - }) -# --- diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index b6bbe671996e21..52a09c5511d97c 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -21,12 +21,7 @@ SERVICE_SET_TEMPERATURE, HVACMode, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - Platform, - UnitOfTemperature, -) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.entity_registry as er @@ -44,10 +39,10 @@ @pytest.mark.parametrize( - ("static_file", "temperature_unit"), + ("static_file"), [ - ("static.json", UnitOfTemperature.CELSIUS), - ("static_F.json", UnitOfTemperature.FAHRENHEIT), + ("static.json"), + ("static_F.json"), ], ) async def test_celsius_fahrenheit( @@ -57,7 +52,6 @@ async def test_celsius_fahrenheit( snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, static_file: str, - temperature_unit: str, ) -> None: """Test Celsius and Fahrenheit temperature units.""" # Load static data from fixture @@ -135,7 +129,6 @@ async def test_climate_entity_properties( assert state.attributes["preset_mode"] == PRESET_ECO -@pytest.mark.parametrize("static_file", ["static.json"]) @pytest.mark.parametrize( "mode", [HVACMode.HEAT, HVACMode.AUTO, HVACMode.OFF], @@ -144,15 +137,10 @@ async def test_async_set_hvac_mode( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, - static_file: str, mode: HVACMode, ) -> None: """Test setting HVAC mode via service call.""" - static_data = json.loads(load_fixture(static_file, DOMAIN)) - with patch.object( - mock_bsblan, "static_values", return_value=StaticState.from_dict(static_data) - ): - await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) # Call the service to set HVAC mode await hass.services.async_call( @@ -168,22 +156,52 @@ async def test_async_set_hvac_mode( @pytest.mark.parametrize( - ("hvac_mode", "preset_mode", "expected_success"), + ("hvac_mode", "preset_mode"), [ - (HVACMode.AUTO, PRESET_ECO, True), - (HVACMode.AUTO, PRESET_NONE, True), - (HVACMode.HEAT, PRESET_ECO, False), + (HVACMode.AUTO, PRESET_ECO), + (HVACMode.AUTO, PRESET_NONE), ], ) -async def test_async_set_preset_mode( +async def test_async_set_preset_mode_succes( + hass: HomeAssistant, + mock_bsblan: AsyncMock, + mock_config_entry: MockConfigEntry, + hvac_mode: HVACMode, + preset_mode: str, +) -> None: + """Test setting preset mode via service call.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + # patch hvac_mode + mock_hvac_mode = MagicMock() + mock_hvac_mode.value = hvac_mode + mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode + + # Attempt to set the preset mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset_mode}, + blocking=True, + ) + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + ("hvac_mode", "preset_mode"), + [ + ( + HVACMode.HEAT, + PRESET_ECO, + ) + ], +) +async def test_async_set_preset_mode_error( hass: HomeAssistant, mock_bsblan: AsyncMock, mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, hvac_mode: HVACMode, preset_mode: str, - expected_success: bool, - freezer: FrozenDateTimeFactory, ) -> None: """Test setting preset mode via service call.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) @@ -194,26 +212,14 @@ async def test_async_set_preset_mode( mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode # Attempt to set the preset mode - if not expected_success: - with pytest.raises(HomeAssistantError) as exc_info: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset_mode}, - blocking=True, - ) - assert "set_preset_mode_error" in str(exc_info.value) - else: + ERROR_MSG = "Preset mode can only be set when HVAC mode is set to 'auto'" + with pytest.raises(HomeAssistantError, match=ERROR_MSG): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset_mode}, blocking=True, ) - await hass.async_block_till_done() - - # Reset the mock for the next iteration - mock_bsblan.thermostat.reset_mock() @pytest.mark.parametrize( @@ -242,8 +248,6 @@ async def test_async_set_temperature( # Assert that the thermostat method was called with the correct temperature mock_bsblan.thermostat.assert_called_once_with(target_temperature=target_temp) - mock_bsblan.thermostat.reset_mock() - async def test_async_set_data( hass: HomeAssistant, @@ -300,12 +304,11 @@ async def test_async_set_data( # Test error handling mock_bsblan.thermostat.side_effect = BSBLANError("Test error") - with pytest.raises(HomeAssistantError) as exc_info: + ERROR_MSG = "An error occurred while updating the BSBLAN device" + with pytest.raises(HomeAssistantError, match=ERROR_MSG): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 20}, blocking=True, ) - assert "An error occurred while updating the BSBLAN device" in str(exc_info.value) - assert exc_info.value.translation_key == "set_data_error" From e7eef80a8470cbfc10b5af66b0faf20ccfa3754d Mon Sep 17 00:00:00 2001 From: Joostlek Date: Sun, 8 Sep 2024 09:51:23 +0200 Subject: [PATCH 30/30] Fix --- tests/components/bsblan/test_climate.py | 35 ++++++++++--------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/tests/components/bsblan/test_climate.py b/tests/components/bsblan/test_climate.py index 52a09c5511d97c..c519c3043da6f6 100644 --- a/tests/components/bsblan/test_climate.py +++ b/tests/components/bsblan/test_climate.py @@ -1,8 +1,7 @@ """Tests for the BSB-Lan climate platform.""" from datetime import timedelta -import json -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock from bsblan import BSBLANError, StaticState from freezegun.api import FrozenDateTimeFactory @@ -31,7 +30,7 @@ from tests.common import ( MockConfigEntry, async_fire_time_changed, - load_fixture, + load_json_object_fixture, snapshot_platform, ) @@ -54,20 +53,14 @@ async def test_celsius_fahrenheit( static_file: str, ) -> None: """Test Celsius and Fahrenheit temperature units.""" - # Load static data from fixture - static_data = json.loads(load_fixture(static_file, DOMAIN)) - - # Patch the static_values method to return our test data - with patch.object( - mock_bsblan, "static_values", return_value=StaticState.from_dict(static_data) - ): - # Set up the climate platform - await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) - - # Take a snapshot of the entity registry - await snapshot_platform( - hass, entity_registry, snapshot, mock_config_entry.entry_id - ) + + static_data = load_json_object_fixture(static_file, DOMAIN) + + mock_bsblan.static_values.return_value = StaticState.from_dict(static_data) + + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE]) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) async def test_climate_entity_properties( @@ -212,8 +205,8 @@ async def test_async_set_preset_mode_error( mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode # Attempt to set the preset mode - ERROR_MSG = "Preset mode can only be set when HVAC mode is set to 'auto'" - with pytest.raises(HomeAssistantError, match=ERROR_MSG): + error_message = "Preset mode can only be set when HVAC mode is set to 'auto'" + with pytest.raises(HomeAssistantError, match=error_message): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -304,8 +297,8 @@ async def test_async_set_data( # Test error handling mock_bsblan.thermostat.side_effect = BSBLANError("Test error") - ERROR_MSG = "An error occurred while updating the BSBLAN device" - with pytest.raises(HomeAssistantError, match=ERROR_MSG): + error_message = "An error occurred while updating the BSBLAN device" + with pytest.raises(HomeAssistantError, match=error_message): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE,