Compare commits

...

16 Commits

Author SHA1 Message Date
J. Nick Koston
a3e692eb60 Address review feedback: round setpoints and fix test docstring 2026-02-19 16:01:12 -06:00
J. Nick Koston
3339016f32 Only report heating/cooling action when system is running 2026-02-19 13:55:40 -06:00
J. Nick Koston
132165b606 Handle all heating/cooling active states (0, 1, 2) 2026-02-19 13:54:38 -06:00
J. Nick Koston
7505ca8f84 Test cooling takes priority when both active 2026-02-19 13:41:29 -06:00
J. Nick Koston
dc0b1f8f1d Prioritize cooling over heating in hvac_action 2026-02-19 13:40:27 -06:00
J. Nick Koston
acd6affdcb Filter hvac_action by zone mode and improve test coverage
A zone in COOL mode should never report HEATING and vice versa.
Also adds tests for empty protocol fields and cool mode temperature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:38:00 -06:00
J. Nick Koston
cc7cfd68c4 comemnt 2026-02-19 13:30:16 -06:00
J. Nick Koston
4316821f9f tweaks, cover empty cases 2026-02-19 13:27:36 -06:00
J. Nick Koston
4b6907f8e0 abc 2026-02-19 13:23:52 -06:00
J. Nick Koston
55473b88a7 Add docstring comments explaining string protocol fields 2026-02-19 13:18:38 -06:00
J. Nick Koston
7af8a490be Address review: remove branching in tests, drop redundant test
- Remove test_hvac_mode_heat_cool (already covered by snapshot)
- Split test_set_hvac_mode: separate OFF case from parametrized modes
- Split test_turn_on_off into test_turn_on and test_turn_off
- Remove unnecessary float() wrapper in current_humidity
2026-02-19 13:17:12 -06:00
J. Nick Koston
6313b5a0b8 Fix heating/cooling active check to compare against "1"
Raw protocol sends "0" for inactive which is truthy in Python.
Use explicit == "1" comparison instead of truthiness check.
2026-02-19 13:12:49 -06:00
J. Nick Koston
2aae02ba47 trane 2026-02-19 13:02:51 -06:00
J. Nick Koston
f03f4cfac2 tweaks 2026-02-19 12:59:33 -06:00
J. Nick Koston
28a4a6dc07 tweaks 2026-02-19 12:56:43 -06:00
J. Nick Koston
82f1ea773f Add climate platform and address review nits
- Add climate entity with HVAC mode, fan mode, temperature control
- Remove VERSION = 1 from config flow (default)
- Move PLATFORMS from const.py to __init__.py
- Add platform fixture pattern for snapshot test isolation
2026-02-19 12:55:05 -06:00
9 changed files with 666 additions and 10 deletions

View File

@@ -8,14 +8,16 @@ from steamloop import (
ThermostatConnection,
)
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .const import CONF_SECRET_KEY, DOMAIN, MANUFACTURER, PLATFORMS
from .const import CONF_SECRET_KEY, DOMAIN, MANUFACTURER
from .types import TraneConfigEntry
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: TraneConfigEntry) -> bool:
"""Set up Trane Local from a config entry."""

View File

@@ -0,0 +1,200 @@
"""Climate platform for the Trane Local integration."""
from __future__ import annotations
from typing import Any
from steamloop import FanMode, HoldType, ThermostatConnection, ZoneMode
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import TraneZoneEntity
from .types import TraneConfigEntry
PARALLEL_UPDATES = 0
HA_TO_ZONE_MODE = {
HVACMode.OFF: ZoneMode.OFF,
HVACMode.HEAT: ZoneMode.HEAT,
HVACMode.COOL: ZoneMode.COOL,
HVACMode.HEAT_COOL: ZoneMode.AUTO,
HVACMode.AUTO: ZoneMode.AUTO,
}
ZONE_MODE_TO_HA = {
ZoneMode.OFF: HVACMode.OFF,
ZoneMode.HEAT: HVACMode.HEAT,
ZoneMode.COOL: HVACMode.COOL,
ZoneMode.AUTO: HVACMode.AUTO,
}
HA_TO_FAN_MODE = {
"auto": FanMode.AUTO,
"on": FanMode.ALWAYS_ON,
"circulate": FanMode.CIRCULATE,
}
FAN_MODE_TO_HA = {v: k for k, v in HA_TO_FAN_MODE.items()}
SINGLE_SETPOINT_MODES = frozenset({ZoneMode.COOL, ZoneMode.HEAT})
async def async_setup_entry(
hass: HomeAssistant,
config_entry: TraneConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Trane Local climate entities."""
conn = config_entry.runtime_data
async_add_entities(
TraneClimateEntity(conn, config_entry.entry_id, zone_id)
for zone_id in conn.state.zones
)
class TraneClimateEntity(TraneZoneEntity, ClimateEntity):
"""Climate entity for a Trane thermostat zone."""
_attr_name = None
_attr_translation_key = "zone"
_attr_fan_modes = list(HA_TO_FAN_MODE)
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
_attr_target_temperature_step = 1.0
def __init__(self, conn: ThermostatConnection, entry_id: str, zone_id: str) -> None:
"""Initialize the climate entity."""
super().__init__(conn, entry_id, zone_id, "zone")
modes: list[HVACMode] = []
for zone_mode in conn.state.supported_modes:
ha_mode = ZONE_MODE_TO_HA.get(zone_mode)
if ha_mode is None:
continue
modes.append(ha_mode)
# AUTO in steamloop maps to both AUTO (schedule) and HEAT_COOL (manual hold)
if zone_mode == ZoneMode.AUTO:
modes.append(HVACMode.HEAT_COOL)
self._attr_hvac_modes = modes
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
# indoor_temperature is a string from the protocol (e.g. "72.00")
# or empty string if not yet received
if temp := self._zone.indoor_temperature:
return float(temp)
return None
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
# relative_humidity is a string from the protocol (e.g. "45")
# or empty string if not yet received
if humidity := self._conn.state.relative_humidity:
return int(humidity)
return None
@property
def hvac_mode(self) -> HVACMode:
"""Return the current HVAC mode."""
zone = self._zone
if zone.mode == ZoneMode.AUTO and zone.hold_type == HoldType.MANUAL:
return HVACMode.HEAT_COOL
return ZONE_MODE_TO_HA.get(zone.mode, HVACMode.OFF)
@property
def hvac_action(self) -> HVACAction:
"""Return the current HVAC action."""
# heating_active and cooling_active are system-level strings from the
# protocol ("0"=off, "1"=idle, "2"=running); filter by zone mode so
# a zone in COOL never reports HEATING and vice versa
zone_mode = self._zone.mode
if zone_mode == ZoneMode.OFF:
return HVACAction.OFF
state = self._conn.state
if zone_mode != ZoneMode.HEAT and state.cooling_active == "2":
return HVACAction.COOLING
if zone_mode != ZoneMode.COOL and state.heating_active == "2":
return HVACAction.HEATING
return HVACAction.IDLE
@property
def target_temperature(self) -> float | None:
"""Return target temperature for single-setpoint modes."""
# Setpoints are strings from the protocol or empty string if not yet received
zone = self._zone
if zone.mode == ZoneMode.COOL:
return float(zone.cool_setpoint) if zone.cool_setpoint else None
if zone.mode == ZoneMode.HEAT:
return float(zone.heat_setpoint) if zone.heat_setpoint else None
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the upper bound target temperature."""
zone = self._zone
if zone.mode in SINGLE_SETPOINT_MODES:
return None
return float(zone.cool_setpoint) if zone.cool_setpoint else None
@property
def target_temperature_low(self) -> float | None:
"""Return the lower bound target temperature."""
zone = self._zone
if zone.mode in SINGLE_SETPOINT_MODES:
return None
return float(zone.heat_setpoint) if zone.heat_setpoint else None
@property
def fan_mode(self) -> str:
"""Return the current fan mode."""
return FAN_MODE_TO_HA.get(self._conn.state.fan_mode, "auto")
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
if hvac_mode == HVACMode.OFF:
self._conn.set_zone_mode(self._zone_id, ZoneMode.OFF)
return
hold_type = HoldType.SCHEDULE if hvac_mode == HVACMode.AUTO else HoldType.MANUAL
self._conn.set_temperature_setpoint(self._zone_id, hold_type=hold_type)
self._conn.set_zone_mode(self._zone_id, HA_TO_ZONE_MODE[hvac_mode])
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set target temperature."""
heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
set_temp = kwargs.get(ATTR_TEMPERATURE)
if set_temp is not None:
if self._zone.mode == ZoneMode.COOL:
cool_temp = set_temp
elif self._zone.mode == ZoneMode.HEAT:
heat_temp = set_temp
self._conn.set_temperature_setpoint(
self._zone_id,
heat_setpoint=str(round(heat_temp)) if heat_temp is not None else None,
cool_setpoint=str(round(cool_temp)) if cool_temp is not None else None,
)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the fan mode."""
self._conn.set_fan_mode(HA_TO_FAN_MODE[fan_mode])

View File

@@ -25,8 +25,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class TraneConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Trane Local."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:

View File

@@ -1,11 +1,7 @@
"""Constants for the Trane Local integration."""
from homeassistant.const import Platform
DOMAIN = "trane"
PLATFORMS = [Platform.SWITCH]
CONF_SECRET_KEY = "secret_key"
MANUFACTURER = "Trane"

View File

@@ -25,6 +25,19 @@
}
},
"entity": {
"climate": {
"zone": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "[%key:common::state::auto%]",
"circulate": "Circulate",
"on": "[%key:common::state::on%]"
}
}
}
}
},
"switch": {
"hold": {
"name": "Hold"

View File

@@ -1,13 +1,14 @@
"""Fixtures for the Trane Local integration tests."""
from collections.abc import Generator
from collections.abc import AsyncGenerator, Generator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from steamloop import FanMode, HoldType, ThermostatState, Zone, ZoneMode
from homeassistant.components.trane import PLATFORMS
from homeassistant.components.trane.const import CONF_SECRET_KEY, DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -31,6 +32,19 @@ def mock_config_entry() -> MockConfigEntry:
)
@pytest.fixture
def platforms() -> list[Platform]:
"""Platforms, which should be loaded during the test."""
return PLATFORMS
@pytest.fixture(autouse=True)
async def mock_patch_platforms(platforms: list[Platform]) -> AsyncGenerator[None]:
"""Fixture to set up platforms for tests."""
with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", platforms):
yield
def _make_state() -> ThermostatState:
"""Create a mock thermostat state."""
return ThermostatState(
@@ -49,6 +63,8 @@ def _make_state() -> ThermostatState:
supported_modes=[ZoneMode.OFF, ZoneMode.AUTO, ZoneMode.COOL, ZoneMode.HEAT],
fan_mode=FanMode.AUTO,
relative_humidity="45",
heating_active="0",
cooling_active="0",
)

View File

@@ -0,0 +1,89 @@
# serializer version: 1
# name: test_climate_entities[climate.living_room-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
'auto',
'on',
'circulate',
]),
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 95,
'min_temp': 45,
'target_temp_step': 1.0,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.living_room',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'trane',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 395>,
'translation_key': 'zone',
'unique_id': 'test_entry_id_1_zone',
'unit_of_measurement': None,
})
# ---
# name: test_climate_entities[climate.living_room-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 45,
'current_temperature': 72,
'fan_mode': 'auto',
'fan_modes': list([
'auto',
'on',
'circulate',
]),
'friendly_name': 'Living Room',
'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 95,
'min_temp': 45,
'supported_features': <ClimateEntityFeature: 395>,
'target_temp_high': 76,
'target_temp_low': 68,
'target_temp_step': 1.0,
'temperature': None,
}),
'context': <ANY>,
'entity_id': 'climate.living_room',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat_cool',
})
# ---

View File

@@ -0,0 +1,335 @@
"""Tests for the Trane Local climate platform."""
from unittest.mock import MagicMock
import pytest
from steamloop import FanMode, HoldType, ZoneMode
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_TEMPERATURE,
HVACAction,
HVACMode,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Platforms, which should be loaded during the test."""
return [Platform.CLIMATE]
@pytest.fixture(autouse=True)
def set_us_customary(hass: HomeAssistant) -> None:
"""Set US customary unit system for Trane (Fahrenheit thermostats)."""
hass.config.units = US_CUSTOMARY_SYSTEM
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_climate_entities(
hass: HomeAssistant,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Snapshot all climate entities."""
await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id)
async def test_hvac_mode_auto(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test HVAC mode is AUTO when following schedule."""
mock_connection.state.zones["1"].hold_type = HoldType.SCHEDULE
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.living_room")
assert state is not None
assert state.state == HVACMode.AUTO
async def test_current_temperature_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test current temperature is None when not yet received."""
mock_connection.state.zones["1"].indoor_temperature = ""
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.living_room")
assert state is not None
assert state.attributes["current_temperature"] is None
async def test_current_humidity_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test current humidity is omitted when not yet received."""
mock_connection.state.relative_humidity = ""
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.living_room")
assert state is not None
assert "current_humidity" not in state.attributes
async def test_set_hvac_mode_off(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setting HVAC mode to off."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVACMode.OFF},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_not_called()
mock_connection.set_zone_mode.assert_called_once_with("1", ZoneMode.OFF)
@pytest.mark.parametrize(
("hvac_mode", "expected_hold", "expected_zone_mode"),
[
(HVACMode.AUTO, HoldType.SCHEDULE, ZoneMode.AUTO),
(HVACMode.HEAT_COOL, HoldType.MANUAL, ZoneMode.AUTO),
(HVACMode.HEAT, HoldType.MANUAL, ZoneMode.HEAT),
(HVACMode.COOL, HoldType.MANUAL, ZoneMode.COOL),
],
)
async def test_set_hvac_mode(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
hvac_mode: HVACMode,
expected_hold: HoldType,
expected_zone_mode: ZoneMode,
) -> None:
"""Test setting HVAC mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: hvac_mode},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1", hold_type=expected_hold
)
mock_connection.set_zone_mode.assert_called_once_with("1", expected_zone_mode)
async def test_set_temperature_range(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setting temperature range in heat_cool mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.living_room",
ATTR_TARGET_TEMP_LOW: 65,
ATTR_TARGET_TEMP_HIGH: 78,
},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1",
heat_setpoint="65",
cool_setpoint="78",
)
async def test_set_temperature_single_heat(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setting single temperature in heat mode."""
mock_connection.state.zones["1"].mode = ZoneMode.HEAT
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.living_room",
ATTR_TEMPERATURE: 70,
},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1",
heat_setpoint="70",
cool_setpoint=None,
)
async def test_set_temperature_single_cool(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setting single temperature in cool mode."""
mock_connection.state.zones["1"].mode = ZoneMode.COOL
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.living_room",
ATTR_TEMPERATURE: 78,
},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1",
heat_setpoint=None,
cool_setpoint="78",
)
@pytest.mark.parametrize(
("fan_mode", "expected_fan_mode"),
[
("auto", FanMode.AUTO),
("on", FanMode.ALWAYS_ON),
("circulate", FanMode.CIRCULATE),
],
)
async def test_set_fan_mode(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
fan_mode: str,
expected_fan_mode: FanMode,
) -> None:
"""Test setting fan mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: fan_mode},
blocking=True,
)
mock_connection.set_fan_mode.assert_called_once_with(expected_fan_mode)
@pytest.mark.parametrize(
("cooling_active", "heating_active", "zone_mode", "expected_action"),
[
("0", "0", ZoneMode.OFF, HVACAction.OFF),
("0", "2", ZoneMode.AUTO, HVACAction.HEATING),
("2", "0", ZoneMode.AUTO, HVACAction.COOLING),
("0", "0", ZoneMode.AUTO, HVACAction.IDLE),
("0", "1", ZoneMode.AUTO, HVACAction.IDLE),
("1", "0", ZoneMode.AUTO, HVACAction.IDLE),
("0", "2", ZoneMode.COOL, HVACAction.IDLE),
("2", "0", ZoneMode.HEAT, HVACAction.IDLE),
("2", "2", ZoneMode.AUTO, HVACAction.COOLING),
],
)
async def test_hvac_action(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
cooling_active: str,
heating_active: str,
zone_mode: ZoneMode,
expected_action: HVACAction,
) -> None:
"""Test HVAC action reflects thermostat state."""
mock_connection.state.cooling_active = cooling_active
mock_connection.state.heating_active = heating_active
mock_connection.state.zones["1"].mode = zone_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()
state = hass.states.get("climate.living_room")
assert state is not None
assert state.attributes["hvac_action"] == expected_action
async def test_turn_on(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test turn on defaults to heat_cool mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "climate.living_room"},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1", hold_type=HoldType.MANUAL
)
mock_connection.set_zone_mode.assert_called_once_with("1", ZoneMode.AUTO)
async def test_turn_off(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test turn off sets mode to off."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "climate.living_room"},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_not_called()
mock_connection.set_zone_mode.assert_called_once_with("1", ZoneMode.OFF)

View File

@@ -12,6 +12,7 @@ from homeassistant.const import (
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -19,6 +20,12 @@ from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Platforms, which should be loaded during the test."""
return [Platform.SWITCH]
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_switch_entities(
hass: HomeAssistant,