Files

315 lines
9.7 KiB
Python

"""Tests for the Nobø Ecohub climate platform."""
from unittest.mock import MagicMock
from pynobo import PynoboError, nobo
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE,
ATTR_HVAC_MODE,
ATTR_PRESET_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DOMAIN as CLIMATE_DOMAIN,
PRESET_AWAY,
PRESET_COMFORT,
PRESET_ECO,
PRESET_NONE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
HVACMode,
)
from homeassistant.components.nobo_hub.const import (
CONF_OVERRIDE_TYPE,
DOMAIN,
OVERRIDE_TYPE_NOW,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import entity_unique_ids, fire_hub_update
from .conftest import SERIAL
from tests.common import MockConfigEntry, snapshot_platform
CLIMATE_ENTITY = "climate.living_room_living_room"
BEDROOM_ZONE = {
"zone_id": "2",
"name": "Bedroom",
"week_profile_id": "0",
"temp_comfort_c": "22",
"temp_eco_c": "18",
}
@pytest.fixture
def platforms() -> list[Platform]:
"""Only set up the climate platform for these tests."""
return [Platform.CLIMATE]
@pytest.mark.usefixtures("init_integration")
async def test_climate_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
) -> None:
"""All climate entities match their snapshot."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("zone_mode", "expected_state", "expected_preset"),
[
(nobo.API.NAME_OFF, HVACMode.OFF, PRESET_NONE),
(nobo.API.NAME_AWAY, HVACMode.AUTO, PRESET_AWAY),
(nobo.API.NAME_ECO, HVACMode.AUTO, PRESET_ECO),
(nobo.API.NAME_COMFORT, HVACMode.AUTO, PRESET_COMFORT),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_state_maps_zone_mode(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
zone_mode: str,
expected_state: HVACMode,
expected_preset: str,
) -> None:
"""Zone modes map to the expected HVAC mode and preset."""
mock_nobo_hub.get_current_zone_mode.return_value = zone_mode
await fire_hub_update(hass, mock_nobo_hub)
state = hass.states.get(CLIMATE_ENTITY)
assert state.state == expected_state
assert state.attributes[ATTR_PRESET_MODE] == expected_preset
@pytest.mark.usefixtures("init_integration")
async def test_state_override_forces_heat(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
) -> None:
"""A non-normal zone override maps to HVACMode.HEAT."""
# Any non-NORMAL override value suffices; NAME_COMFORT is arbitrary.
mock_nobo_hub.get_zone_override_mode.return_value = nobo.API.NAME_COMFORT
await fire_hub_update(hass, mock_nobo_hub)
assert hass.states.get(CLIMATE_ENTITY).state == HVACMode.HEAT
@pytest.mark.usefixtures("init_integration")
async def test_current_temperature_unknown_when_missing(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
) -> None:
"""A missing current temperature surfaces as None."""
mock_nobo_hub.get_current_zone_temperature.return_value = None
await fire_hub_update(hass, mock_nobo_hub)
assert hass.states.get(CLIMATE_ENTITY).attributes[ATTR_CURRENT_TEMPERATURE] is None
@pytest.mark.parametrize(
("hvac_mode", "expected_override"),
[
(HVACMode.AUTO, nobo.API.OVERRIDE_MODE_NORMAL),
(HVACMode.HEAT, nobo.API.OVERRIDE_MODE_COMFORT),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_set_hvac_mode(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
hvac_mode: HVACMode,
expected_override: str,
) -> None:
"""Each HVAC mode maps to the expected zone override."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: CLIMATE_ENTITY, ATTR_HVAC_MODE: hvac_mode},
blocking=True,
)
mock_nobo_hub.async_create_override.assert_called_once_with(
expected_override,
nobo.API.OVERRIDE_TYPE_CONSTANT,
nobo.API.OVERRIDE_TARGET_ZONE,
"1",
)
@pytest.mark.parametrize(
("preset", "expected_mode"),
[
(PRESET_NONE, nobo.API.OVERRIDE_MODE_NORMAL),
(PRESET_COMFORT, nobo.API.OVERRIDE_MODE_COMFORT),
(PRESET_ECO, nobo.API.OVERRIDE_MODE_ECO),
(PRESET_AWAY, nobo.API.OVERRIDE_MODE_AWAY),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_set_preset_mode(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
preset: str,
expected_mode: str,
) -> None:
"""Each preset maps to the expected override mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: CLIMATE_ENTITY, ATTR_PRESET_MODE: preset},
blocking=True,
)
mock_nobo_hub.async_create_override.assert_called_once_with(
expected_mode,
nobo.API.OVERRIDE_TYPE_CONSTANT,
nobo.API.OVERRIDE_TARGET_ZONE,
"1",
)
@pytest.mark.parametrize(
"config_entry_options",
[{CONF_OVERRIDE_TYPE: OVERRIDE_TYPE_NOW}],
)
@pytest.mark.usefixtures("init_integration")
async def test_set_preset_with_override_type_now(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
) -> None:
"""The override_type option flows into the zone override call."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: CLIMATE_ENTITY, ATTR_PRESET_MODE: PRESET_COMFORT},
blocking=True,
)
mock_nobo_hub.async_create_override.assert_called_once_with(
nobo.API.OVERRIDE_MODE_COMFORT,
nobo.API.OVERRIDE_TYPE_NOW,
nobo.API.OVERRIDE_TARGET_ZONE,
"1",
)
@pytest.mark.usefixtures("init_integration")
async def test_zone_removed_removes_entity(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
) -> None:
"""Removing a zone via the Nobø app must not crash and removes the entity."""
mock_nobo_hub.zones.pop("1")
await fire_hub_update(hass, mock_nobo_hub)
assert hass.states.get(CLIMATE_ENTITY) is None
@pytest.mark.usefixtures("init_integration")
async def test_set_temperature_updates_zone(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
) -> None:
"""Setting target temperatures updates the zone on the hub."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: CLIMATE_ENTITY,
ATTR_TARGET_TEMP_LOW: 16.4,
ATTR_TARGET_TEMP_HIGH: 21.6,
},
blocking=True,
)
mock_nobo_hub.async_update_zone.assert_called_once_with(
"1", temp_comfort_c=22, temp_eco_c=16
)
@pytest.mark.parametrize(
("service", "service_data", "mock_attr", "expected_key"),
[
(
SERVICE_SET_HVAC_MODE,
{ATTR_HVAC_MODE: HVACMode.HEAT},
"async_create_override",
"set_hvac_mode_failed",
),
(
SERVICE_SET_PRESET_MODE,
{ATTR_PRESET_MODE: PRESET_COMFORT},
"async_create_override",
"set_preset_mode_failed",
),
(
SERVICE_SET_TEMPERATURE,
{ATTR_TARGET_TEMP_LOW: 17, ATTR_TARGET_TEMP_HIGH: 21},
"async_update_zone",
"set_temperature_failed",
),
],
ids=["set_hvac_mode", "set_preset_mode", "set_temperature"],
)
@pytest.mark.usefixtures("init_integration")
async def test_climate_action_wraps_library_error(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
service: str,
service_data: dict[str, object],
mock_attr: str,
expected_key: str,
) -> None:
"""Library errors during climate actions are raised as HomeAssistantError."""
getattr(mock_nobo_hub, mock_attr).side_effect = PynoboError("boom")
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
CLIMATE_DOMAIN,
service,
{ATTR_ENTITY_ID: CLIMATE_ENTITY, **service_data},
blocking=True,
)
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == expected_key
@pytest.mark.usefixtures("init_integration")
async def test_new_zone_adds_entity(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""A zone added on the hub at runtime creates a climate entity."""
entry_id = mock_config_entry.entry_id
assert f"{SERIAL}:2" not in entity_unique_ids(entity_registry, entry_id)
mock_nobo_hub.zones["2"] = BEDROOM_ZONE
await fire_hub_update(hass, mock_nobo_hub)
assert f"{SERIAL}:2" in entity_unique_ids(entity_registry, entry_id)
@pytest.mark.usefixtures("init_integration")
async def test_readded_zone_reappears(
hass: HomeAssistant,
mock_nobo_hub: MagicMock,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""A zone removed and re-added under the same id (the hub reuses ids) reappears."""
entry_id = mock_config_entry.entry_id
mock_nobo_hub.zones["2"] = BEDROOM_ZONE
await fire_hub_update(hass, mock_nobo_hub)
assert f"{SERIAL}:2" in entity_unique_ids(entity_registry, entry_id)
del mock_nobo_hub.zones["2"]
await fire_hub_update(hass, mock_nobo_hub)
assert f"{SERIAL}:2" not in entity_unique_ids(entity_registry, entry_id)
mock_nobo_hub.zones["2"] = BEDROOM_ZONE
await fire_hub_update(hass, mock_nobo_hub)
assert f"{SERIAL}:2" in entity_unique_ids(entity_registry, entry_id)