mirror of
https://github.com/home-assistant/core.git
synced 2025-12-11 02:18:34 +00:00
270 lines
8.3 KiB
Python
270 lines
8.3 KiB
Python
"""Test the Airobot climate platform."""
|
|
|
|
from datetime import timedelta
|
|
from unittest.mock import AsyncMock
|
|
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
from pyairobotrest.const import MODE_AWAY, MODE_HOME
|
|
from pyairobotrest.exceptions import AirobotConnectionError, AirobotError
|
|
from pyairobotrest.models import ThermostatSettings, ThermostatStatus
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
|
|
from homeassistant.components.climate import (
|
|
ATTR_PRESET_MODE,
|
|
ATTR_TEMPERATURE,
|
|
DOMAIN as CLIMATE_DOMAIN,
|
|
SERVICE_SET_PRESET_MODE,
|
|
SERVICE_SET_TEMPERATURE,
|
|
)
|
|
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ServiceValidationError
|
|
import homeassistant.helpers.entity_registry as er
|
|
|
|
from tests.common import MockConfigEntry, snapshot_platform
|
|
|
|
|
|
@pytest.fixture
|
|
def platforms() -> list[Platform]:
|
|
"""Fixture to specify platforms to test."""
|
|
return [Platform.CLIMATE]
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_climate_entities(
|
|
hass: HomeAssistant,
|
|
snapshot: SnapshotAssertion,
|
|
entity_registry: er.EntityRegistry,
|
|
mock_config_entry: MockConfigEntry,
|
|
platforms: list[Platform],
|
|
) -> None:
|
|
"""Test climate entities."""
|
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mode", "temperature", "method"),
|
|
[
|
|
(1, 24.0, "set_home_temperature"), # Home mode
|
|
(0, 18.0, "set_away_temperature"), # Away mode
|
|
],
|
|
)
|
|
async def test_climate_set_temperature(
|
|
hass: HomeAssistant,
|
|
mock_airobot_client: AsyncMock,
|
|
mock_settings: ThermostatSettings,
|
|
mock_config_entry: MockConfigEntry,
|
|
mode: int,
|
|
temperature: float,
|
|
method: str,
|
|
) -> None:
|
|
"""Test setting temperature in different modes."""
|
|
# Set device mode
|
|
mock_settings.mode = mode
|
|
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
await hass.services.async_call(
|
|
CLIMATE_DOMAIN,
|
|
SERVICE_SET_TEMPERATURE,
|
|
{
|
|
ATTR_ENTITY_ID: "climate.test_thermostat",
|
|
ATTR_TEMPERATURE: temperature,
|
|
},
|
|
blocking=True,
|
|
)
|
|
|
|
getattr(mock_airobot_client, method).assert_called_once_with(temperature)
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_climate_set_temperature_error(
|
|
hass: HomeAssistant,
|
|
mock_airobot_client: AsyncMock,
|
|
) -> None:
|
|
"""Test error handling when setting temperature fails."""
|
|
mock_airobot_client.set_home_temperature.side_effect = AirobotError("Device error")
|
|
|
|
with pytest.raises(
|
|
ServiceValidationError, match="Failed to set temperature"
|
|
) as exc_info:
|
|
await hass.services.async_call(
|
|
CLIMATE_DOMAIN,
|
|
SERVICE_SET_TEMPERATURE,
|
|
{
|
|
ATTR_ENTITY_ID: "climate.test_thermostat",
|
|
ATTR_TEMPERATURE: 24.0,
|
|
},
|
|
blocking=True,
|
|
)
|
|
|
|
assert exc_info.value.translation_domain == "airobot"
|
|
assert exc_info.value.translation_key == "set_temperature_failed"
|
|
assert exc_info.value.translation_placeholders == {"temperature": "24.0"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("preset_mode", "method", "arg"),
|
|
[
|
|
("home", "set_mode", MODE_HOME),
|
|
("away", "set_mode", MODE_AWAY),
|
|
("boost", "set_boost_mode", True),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_climate_set_preset_mode(
|
|
hass: HomeAssistant,
|
|
mock_airobot_client: AsyncMock,
|
|
preset_mode: str,
|
|
method: str,
|
|
arg: int | bool,
|
|
) -> None:
|
|
"""Test setting different preset modes."""
|
|
await hass.services.async_call(
|
|
CLIMATE_DOMAIN,
|
|
SERVICE_SET_PRESET_MODE,
|
|
{
|
|
ATTR_ENTITY_ID: "climate.test_thermostat",
|
|
ATTR_PRESET_MODE: preset_mode,
|
|
},
|
|
blocking=True,
|
|
)
|
|
|
|
getattr(mock_airobot_client, method).assert_called_once_with(arg)
|
|
|
|
|
|
async def test_climate_set_preset_mode_from_boost_to_home(
|
|
hass: HomeAssistant,
|
|
mock_airobot_client: AsyncMock,
|
|
mock_settings: ThermostatSettings,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test disabling boost when switching to home mode."""
|
|
# Set boost mode enabled
|
|
mock_settings.setting_flags.boost_enabled = True
|
|
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
await hass.services.async_call(
|
|
CLIMATE_DOMAIN,
|
|
SERVICE_SET_PRESET_MODE,
|
|
{
|
|
ATTR_ENTITY_ID: "climate.test_thermostat",
|
|
ATTR_PRESET_MODE: "home",
|
|
},
|
|
blocking=True,
|
|
)
|
|
|
|
# Should disable boost first, then set mode to home
|
|
mock_airobot_client.set_boost_mode.assert_called_once_with(False)
|
|
mock_airobot_client.set_mode.assert_called_once_with(MODE_HOME)
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_climate_set_preset_mode_error(
|
|
hass: HomeAssistant,
|
|
mock_airobot_client: AsyncMock,
|
|
) -> None:
|
|
"""Test error handling when setting preset mode fails."""
|
|
mock_airobot_client.set_boost_mode.side_effect = AirobotError("Device error")
|
|
|
|
with pytest.raises(
|
|
ServiceValidationError, match="Failed to set preset mode"
|
|
) as exc_info:
|
|
await hass.services.async_call(
|
|
CLIMATE_DOMAIN,
|
|
SERVICE_SET_PRESET_MODE,
|
|
{
|
|
ATTR_ENTITY_ID: "climate.test_thermostat",
|
|
ATTR_PRESET_MODE: "boost",
|
|
},
|
|
blocking=True,
|
|
)
|
|
|
|
assert exc_info.value.translation_domain == "airobot"
|
|
assert exc_info.value.translation_key == "set_preset_mode_failed"
|
|
assert exc_info.value.translation_placeholders == {"preset_mode": "boost"}
|
|
|
|
|
|
async def test_climate_heating_state(
|
|
hass: HomeAssistant,
|
|
mock_airobot_client: AsyncMock,
|
|
mock_status: ThermostatStatus,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test climate entity shows heating action when heating."""
|
|
# Set heating on
|
|
mock_status.status_flags.heating_on = True
|
|
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("climate.test_thermostat")
|
|
assert state
|
|
assert state.attributes.get("hvac_action") == "heating"
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_climate_unavailable_on_update_failure(
|
|
hass: HomeAssistant,
|
|
mock_airobot_client: AsyncMock,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test climate entity becomes unavailable when coordinator update fails."""
|
|
# Initially available
|
|
state = hass.states.get("climate.test_thermostat")
|
|
assert state
|
|
assert state.state != "unavailable"
|
|
|
|
# Simulate connection error during update
|
|
mock_airobot_client.get_statuses.side_effect = AirobotConnectionError(
|
|
"Connection lost"
|
|
)
|
|
mock_airobot_client.get_settings.side_effect = AirobotConnectionError(
|
|
"Connection lost"
|
|
)
|
|
|
|
# Advance time to trigger coordinator update (30 second interval)
|
|
freezer.tick(timedelta(seconds=35))
|
|
await hass.async_block_till_done()
|
|
|
|
# Entity should now be unavailable
|
|
state = hass.states.get("climate.test_thermostat")
|
|
assert state
|
|
assert state.state == "unavailable"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("temp_floor", "temp_air", "expected_temp"),
|
|
[
|
|
(25.0, 22.0, 25.0), # Floor sensor available - should use floor temp
|
|
(None, 22.0, 22.0), # Floor sensor not available - should use air temp
|
|
],
|
|
)
|
|
async def test_climate_current_temperature(
|
|
hass: HomeAssistant,
|
|
mock_airobot_client: AsyncMock,
|
|
mock_status: ThermostatStatus,
|
|
mock_config_entry: MockConfigEntry,
|
|
temp_floor: float | None,
|
|
temp_air: float,
|
|
expected_temp: float,
|
|
) -> None:
|
|
"""Test current temperature prioritizes floor sensor when available."""
|
|
mock_status.temp_floor = temp_floor
|
|
mock_status.temp_air = temp_air
|
|
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get("climate.test_thermostat")
|
|
assert state
|
|
assert state.attributes.get("current_temperature") == expected_temp
|