mirror of
https://github.com/home-assistant/core.git
synced 2025-12-25 13:38:04 +00:00
345 lines
11 KiB
Python
345 lines
11 KiB
Python
"""Tests for the BSB-Lan climate platform."""
|
|
|
|
from datetime import timedelta
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from bsblan import BSBLANError
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
|
|
from homeassistant.components.climate import (
|
|
ATTR_HVAC_MODE,
|
|
ATTR_PRESET_MODE,
|
|
DOMAIN as CLIMATE_DOMAIN,
|
|
PRESET_ECO,
|
|
PRESET_NONE,
|
|
SERVICE_SET_HVAC_MODE,
|
|
SERVICE_SET_PRESET_MODE,
|
|
SERVICE_SET_TEMPERATURE,
|
|
HVACMode,
|
|
)
|
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
from . import setup_with_selected_platforms
|
|
|
|
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
|
|
|
ENTITY_ID = "climate.bsb_lan"
|
|
|
|
|
|
async def test_celsius_fahrenheit(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
snapshot: SnapshotAssertion,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test Celsius and Fahrenheit temperature units."""
|
|
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(
|
|
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)
|
|
|
|
# Test 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()
|
|
|
|
state = hass.states.get(ENTITY_ID)
|
|
assert state.attributes["temperature"] == 23.5
|
|
|
|
# Test hvac_mode - BSB-Lan returns integer: 1=auto
|
|
mock_hvac_mode = MagicMock()
|
|
mock_hvac_mode.value = 1 # auto mode
|
|
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 - BSB-Lan mode 2 is eco/reduced
|
|
mock_hvac_mode.value = 2 # eco 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.attributes["preset_mode"] == PRESET_ECO
|
|
|
|
|
|
async def test_climate_without_current_temperature_sensor(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test climate entity when current temperature sensor is not available."""
|
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
|
|
|
# Set current_temperature to None to simulate no temperature sensor
|
|
mock_bsblan.state.return_value.current_temperature = None
|
|
|
|
freezer.tick(timedelta(minutes=1))
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
# Should not crash and current_temperature should be None in attributes
|
|
state = hass.states.get(ENTITY_ID)
|
|
assert state is not None
|
|
assert state.attributes["current_temperature"] is None
|
|
|
|
|
|
async def test_climate_without_target_temperature_sensor(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test climate entity when target temperature sensor is not available."""
|
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
|
|
|
# Set target_temperature to None to simulate no temperature sensor
|
|
mock_bsblan.state.return_value.target_temperature = None
|
|
|
|
freezer.tick(timedelta(minutes=1))
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
# Should not crash and target temperature should be None in attributes
|
|
state = hass.states.get(ENTITY_ID)
|
|
assert state is not None
|
|
assert state.attributes["temperature"] is None
|
|
|
|
|
|
async def test_climate_hvac_mode_none_value(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test climate entity when hvac_mode value is None."""
|
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
|
|
|
# Set hvac_mode.value to None
|
|
mock_hvac_mode = MagicMock()
|
|
mock_hvac_mode.value = None
|
|
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 should be unknown when hvac_mode is None
|
|
state = hass.states.get(ENTITY_ID)
|
|
assert state is not None
|
|
assert state.state == "unknown"
|
|
|
|
|
|
async def test_climate_hvac_mode_string_fallback(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test climate entity with string hvac_mode value (fallback path)."""
|
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
|
|
|
# Set hvac_mode.value to a string (non-integer fallback)
|
|
mock_hvac_mode = MagicMock()
|
|
mock_hvac_mode.value = "heat"
|
|
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()
|
|
|
|
# Should parse the string enum value
|
|
state = hass.states.get(ENTITY_ID)
|
|
assert state is not None
|
|
assert state.state == HVACMode.HEAT
|
|
|
|
|
|
# Mapping from HA HVACMode to BSB-Lan integer values for test assertions
|
|
HA_TO_BSBLAN_HVAC_MODE_TEST: dict[HVACMode, int] = {
|
|
HVACMode.OFF: 0,
|
|
HVACMode.AUTO: 1,
|
|
HVACMode.HEAT: 3,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mode",
|
|
[HVACMode.HEAT, HVACMode.AUTO, HVACMode.OFF],
|
|
)
|
|
async def test_async_set_hvac_mode(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
mode: HVACMode,
|
|
) -> None:
|
|
"""Test setting HVAC mode via service call."""
|
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
|
|
|
# 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,
|
|
)
|
|
|
|
# Assert that the thermostat method was called with integer value
|
|
expected_int = HA_TO_BSBLAN_HVAC_MODE_TEST[mode]
|
|
mock_bsblan.thermostat.assert_called_once_with(hvac_mode=expected_int)
|
|
mock_bsblan.thermostat.reset_mock()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("hvac_mode_int", "preset_mode"),
|
|
[
|
|
(1, PRESET_ECO), # 1 = auto mode
|
|
(1, PRESET_NONE), # 1 = auto mode
|
|
(3, PRESET_ECO), # 3 = heat mode - can also set eco preset
|
|
(0, PRESET_ECO), # 0 = off mode - can also set eco preset
|
|
],
|
|
)
|
|
async def test_async_set_preset_mode_success(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
hvac_mode_int: int,
|
|
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 with integer value (BSB-Lan returns integers)
|
|
mock_hvac_mode = MagicMock()
|
|
mock_hvac_mode.value = hvac_mode_int
|
|
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(
|
|
("target_temp"),
|
|
[
|
|
(8.0), # Min temperature
|
|
(15.0), # Mid-range temperature
|
|
(20.0), # Max temperature
|
|
],
|
|
)
|
|
async def test_async_set_temperature(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
target_temp: float,
|
|
) -> None:
|
|
"""Test setting temperature via service call."""
|
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
|
|
|
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)
|
|
|
|
|
|
async def test_async_set_data(
|
|
hass: HomeAssistant,
|
|
mock_bsblan: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test setting data via service calls."""
|
|
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
|
|
|
# Test setting temperature
|
|
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 - should convert to integer (3=heat)
|
|
await hass.services.async_call(
|
|
CLIMATE_DOMAIN,
|
|
SERVICE_SET_HVAC_MODE,
|
|
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
|
|
blocking=True,
|
|
)
|
|
mock_bsblan.thermostat.assert_called_once_with(hvac_mode=3) # 3 = heat
|
|
mock_bsblan.thermostat.reset_mock()
|
|
|
|
# Patch HVAC mode to AUTO (integer 1)
|
|
mock_hvac_mode = MagicMock()
|
|
mock_hvac_mode.value = 1 # 1 = auto mode
|
|
mock_bsblan.state.return_value.hvac_mode = mock_hvac_mode
|
|
|
|
# Test setting preset mode to ECO - should use integer 2
|
|
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=2) # 2 = eco/reduced
|
|
mock_bsblan.thermostat.reset_mock()
|
|
|
|
# Test setting preset mode to NONE - should use integer 1 (auto)
|
|
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_with(hvac_mode=1) # 1 = auto
|
|
mock_bsblan.thermostat.reset_mock()
|
|
|
|
# Test error handling
|
|
mock_bsblan.thermostat.side_effect = BSBLANError("Test error")
|
|
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,
|
|
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 20},
|
|
blocking=True,
|
|
)
|