1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-09 07:45:11 +01:00

Add Trane Local integration (#163301)

This commit is contained in:
J. Nick Koston
2026-02-19 12:39:58 -06:00
committed by GitHub
parent 7f3583587d
commit 03d9c2cf7b
23 changed files with 859 additions and 6 deletions
Generated
+2
View File
@@ -1743,6 +1743,8 @@ build.json @home-assistant/supervisor
/tests/components/trafikverket_train/ @gjohansson-ST
/homeassistant/components/trafikverket_weatherstation/ @gjohansson-ST
/tests/components/trafikverket_weatherstation/ @gjohansson-ST
/homeassistant/components/trane/ @bdraco
/tests/components/trane/ @bdraco
/homeassistant/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
/tests/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
/homeassistant/components/trend/ @jpbede
@@ -0,0 +1,5 @@
{
"domain": "american_standard",
"name": "American Standard",
"integrations": ["nexia", "trane"]
}
+5
View File
@@ -0,0 +1,5 @@
{
"domain": "trane",
"name": "Trane",
"integrations": ["nexia", "trane"]
}
@@ -0,0 +1,63 @@
"""Integration for Trane Local thermostats."""
from __future__ import annotations
from steamloop import (
AuthenticationError,
SteamloopConnectionError,
ThermostatConnection,
)
from homeassistant.const import CONF_HOST
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 .types import TraneConfigEntry
async def async_setup_entry(hass: HomeAssistant, entry: TraneConfigEntry) -> bool:
"""Set up Trane Local from a config entry."""
conn = ThermostatConnection(
entry.data[CONF_HOST],
secret_key=entry.data[CONF_SECRET_KEY],
)
try:
await conn.connect()
await conn.login()
except (SteamloopConnectionError, TimeoutError) as err:
await conn.disconnect()
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except AuthenticationError as err:
await conn.disconnect()
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentication_failed",
) from err
conn.start_background_tasks()
entry.runtime_data = conn
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.entry_id)},
manufacturer=MANUFACTURER,
translation_key="thermostat",
translation_placeholders={"host": entry.data[CONF_HOST]},
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: TraneConfigEntry) -> bool:
"""Unload a Trane Local config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await entry.runtime_data.disconnect()
return unload_ok
@@ -0,0 +1,62 @@
"""Config flow for the Trane Local integration."""
from __future__ import annotations
import logging
from typing import Any
from steamloop import PairingError, SteamloopConnectionError, ThermostatConnection
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from .const import CONF_SECRET_KEY, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
}
)
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:
"""Handle the user step."""
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
self._async_abort_entries_match({CONF_HOST: host})
conn = ThermostatConnection(host, secret_key="")
try:
await conn.connect()
await conn.pair()
except SteamloopConnectionError, PairingError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception during pairing")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=f"Thermostat ({host})",
data={
CONF_HOST: host,
CONF_SECRET_KEY: conn.secret_key,
},
)
finally:
await conn.disconnect()
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)
+11
View File
@@ -0,0 +1,11 @@
"""Constants for the Trane Local integration."""
from homeassistant.const import Platform
DOMAIN = "trane"
PLATFORMS = [Platform.SWITCH]
CONF_SECRET_KEY = "secret_key"
MANUFACTURER = "Trane"
+67
View File
@@ -0,0 +1,67 @@
"""Base entity for the Trane Local integration."""
from __future__ import annotations
from typing import Any
from steamloop import ThermostatConnection, Zone
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, MANUFACTURER
class TraneEntity(Entity):
"""Base class for all Trane entities."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, conn: ThermostatConnection) -> None:
"""Initialize the entity."""
self._conn = conn
async def async_added_to_hass(self) -> None:
"""Register event callback when added to hass."""
self.async_on_remove(self._conn.add_event_callback(self._handle_event))
@callback
def _handle_event(self, _event: dict[str, Any]) -> None:
"""Handle a thermostat event."""
self.async_write_ha_state()
class TraneZoneEntity(TraneEntity):
"""Base class for Trane zone-level entities."""
def __init__(
self,
conn: ThermostatConnection,
entry_id: str,
zone_id: str,
unique_id_suffix: str,
) -> None:
"""Initialize the entity."""
super().__init__(conn)
self._zone_id = zone_id
self._attr_unique_id = f"{entry_id}_{zone_id}_{unique_id_suffix}"
zone_name = self._zone.name or f"Zone {zone_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry_id}_{zone_id}")},
manufacturer=MANUFACTURER,
name=zone_name,
suggested_area=zone_name,
via_device=(DOMAIN, entry_id),
)
@property
def available(self) -> bool:
"""Return True if the zone is available."""
return self._zone_id in self._conn.state.zones
@property
def _zone(self) -> Zone:
"""Return the current zone state."""
return self._conn.state.zones[self._zone_id]
+12
View File
@@ -0,0 +1,12 @@
{
"entity": {
"switch": {
"hold": {
"default": "mdi:timer",
"state": {
"on": "mdi:timer-off"
}
}
}
}
}
@@ -0,0 +1,12 @@
{
"domain": "trane",
"name": "Trane Local",
"codeowners": ["@bdraco"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/trane",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["steamloop"],
"quality_scale": "bronze",
"requirements": ["steamloop==1.2.0"]
}
@@ -0,0 +1,72 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No custom actions are defined.
appropriate-polling:
status: exempt
comment: |
This is a local push integration that uses event callbacks.
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
@@ -0,0 +1,42 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The IP address of the thermostat"
},
"description": "Put the thermostat in pairing mode (Menu > Settings > Network > Advanced Setup > Remote Connection > Pair). The thermostat must have a static IP address assigned."
}
}
},
"device": {
"thermostat": {
"name": "Thermostat ({host})"
}
},
"entity": {
"switch": {
"hold": {
"name": "Hold"
}
}
},
"exceptions": {
"authentication_failed": {
"message": "Authentication failed with thermostat"
},
"cannot_connect": {
"message": "Failed to connect to thermostat"
}
}
}
+52
View File
@@ -0,0 +1,52 @@
"""Switch platform for the Trane Local integration."""
from __future__ import annotations
from typing import Any
from steamloop import HoldType, ThermostatConnection
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import TraneZoneEntity
from .types import TraneConfigEntry
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: TraneConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Trane Local switch entities."""
conn = config_entry.runtime_data
async_add_entities(
TraneHoldSwitch(conn, config_entry.entry_id, zone_id)
for zone_id in conn.state.zones
)
class TraneHoldSwitch(TraneZoneEntity, SwitchEntity):
"""Switch to control the hold mode of a thermostat zone."""
_attr_translation_key = "hold"
def __init__(self, conn: ThermostatConnection, entry_id: str, zone_id: str) -> None:
"""Initialize the hold switch."""
super().__init__(conn, entry_id, zone_id, "hold")
@property
def is_on(self) -> bool:
"""Return true if the zone is in permanent hold."""
return self._zone.hold_type == HoldType.MANUAL
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable permanent hold."""
self._conn.set_temperature_setpoint(self._zone_id, hold_type=HoldType.MANUAL)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Return to schedule."""
self._conn.set_temperature_setpoint(self._zone_id, hold_type=HoldType.SCHEDULE)
+7
View File
@@ -0,0 +1,7 @@
"""Types for the Trane Local integration."""
from steamloop import ThermostatConnection
from homeassistant.config_entries import ConfigEntry
type TraneConfigEntry = ConfigEntry[ThermostatConnection]
+1
View File
@@ -736,6 +736,7 @@ FLOWS = {
"trafikverket_ferry",
"trafikverket_train",
"trafikverket_weatherstation",
"trane",
"transmission",
"triggercmd",
"tuya",
+34 -6
View File
@@ -303,6 +303,23 @@
"config_flow": false,
"iot_class": "local_polling"
},
"american_standard": {
"name": "American Standard",
"integrations": {
"nexia": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling",
"name": "Nexia/American Standard/Trane"
},
"trane": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push",
"name": "Trane Local"
}
}
},
"amp_motorization": {
"name": "AMP Motorization",
"integration_type": "virtual",
@@ -4526,12 +4543,6 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
"nexia": {
"name": "Nexia/American Standard/Trane",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"nexity": {
"name": "Nexity Eug\u00e9nie",
"integration_type": "virtual",
@@ -7185,6 +7196,23 @@
}
}
},
"trane": {
"name": "Trane",
"integrations": {
"nexia": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling",
"name": "Nexia/American Standard/Trane"
},
"trane": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push",
"name": "Trane Local"
}
}
},
"transmission": {
"name": "Transmission",
"integration_type": "service",
+3
View File
@@ -2986,6 +2986,9 @@ starlink-grpc-core==1.2.3
# homeassistant.components.statsd
statsd==3.2.1
# homeassistant.components.trane
steamloop==1.2.0
# homeassistant.components.steam_online
steamodd==4.21
+3
View File
@@ -2516,6 +2516,9 @@ starlink-grpc-core==1.2.3
# homeassistant.components.statsd
statsd==3.2.1
# homeassistant.components.trane
steamloop==1.2.0
# homeassistant.components.steam_online
steamodd==4.21
+1
View File
@@ -0,0 +1 @@
"""Tests for the Trane Local integration."""
+104
View File
@@ -0,0 +1,104 @@
"""Fixtures for the Trane Local integration tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from steamloop import FanMode, HoldType, ThermostatState, Zone, ZoneMode
from homeassistant.components.trane.const import CONF_SECRET_KEY, DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
MOCK_HOST = "192.168.1.100"
MOCK_SECRET_KEY = "test_secret_key"
MOCK_ENTRY_ID = "test_entry_id"
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return a mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
entry_id=MOCK_ENTRY_ID,
title=f"Thermostat ({MOCK_HOST})",
data={
CONF_HOST: MOCK_HOST,
CONF_SECRET_KEY: MOCK_SECRET_KEY,
},
)
def _make_state() -> ThermostatState:
"""Create a mock thermostat state."""
return ThermostatState(
zones={
"1": Zone(
zone_id="1",
name="Living Room",
mode=ZoneMode.AUTO,
indoor_temperature="72",
heat_setpoint="68",
cool_setpoint="76",
deadband="3",
hold_type=HoldType.MANUAL,
),
},
supported_modes=[ZoneMode.OFF, ZoneMode.AUTO, ZoneMode.COOL, ZoneMode.HEAT],
fan_mode=FanMode.AUTO,
relative_humidity="45",
)
@pytest.fixture
def mock_connection() -> Generator[MagicMock]:
"""Return a mocked ThermostatConnection."""
with (
patch(
"homeassistant.components.trane.ThermostatConnection",
autospec=True,
) as mock_cls,
patch(
"homeassistant.components.trane.config_flow.ThermostatConnection",
new=mock_cls,
),
):
conn = mock_cls.return_value
conn.connect = AsyncMock()
conn.login = AsyncMock()
conn.pair = AsyncMock()
conn.disconnect = AsyncMock()
conn.start_background_tasks = MagicMock()
conn.set_temperature_setpoint = MagicMock()
conn.set_zone_mode = MagicMock()
conn.set_fan_mode = MagicMock()
conn.set_emergency_heat = MagicMock()
conn.add_event_callback = MagicMock(return_value=MagicMock())
conn.state = _make_state()
conn.secret_key = MOCK_SECRET_KEY
yield conn
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Mock setup entry."""
with patch(
"homeassistant.components.trane.async_setup_entry",
return_value=True,
) as mock_setup:
yield mock_setup
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> MockConfigEntry:
"""Set up the Trane Local integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry
@@ -0,0 +1,50 @@
# serializer version: 1
# name: test_switch_entities[switch.living_room_hold-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.living_room_hold',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Hold',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Hold',
'platform': 'trane',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'hold',
'unique_id': 'test_entry_id_1_hold',
'unit_of_measurement': None,
})
# ---
# name: test_switch_entities[switch.living_room_hold-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Living Room Hold',
}),
'context': <ANY>,
'entity_id': 'switch.living_room_hold',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
+108
View File
@@ -0,0 +1,108 @@
"""Tests for the Trane Local config flow."""
from unittest.mock import MagicMock
import pytest
from steamloop import PairingError, SteamloopConnectionError
from homeassistant.components.trane.const import CONF_SECRET_KEY, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .conftest import MOCK_HOST, MOCK_SECRET_KEY
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_setup_entry")
async def test_full_user_flow(
hass: HomeAssistant,
mock_connection: MagicMock,
) -> None:
"""Test the full user config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: MOCK_HOST},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Thermostat ({MOCK_HOST})"
assert result["data"] == {
CONF_HOST: MOCK_HOST,
CONF_SECRET_KEY: MOCK_SECRET_KEY,
}
assert result["result"].unique_id is None
@pytest.mark.usefixtures("mock_setup_entry")
@pytest.mark.parametrize(
("side_effect", "error_key"),
[
(SteamloopConnectionError, "cannot_connect"),
(PairingError, "cannot_connect"),
(RuntimeError, "unknown"),
],
)
async def test_form_errors_can_recover(
hass: HomeAssistant,
mock_connection: MagicMock,
side_effect: Exception,
error_key: str,
) -> None:
"""Test errors and recovery during config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
mock_connection.pair.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: MOCK_HOST},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error_key}
mock_connection.pair.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: MOCK_HOST},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Thermostat ({MOCK_HOST})"
assert result["data"] == {
CONF_HOST: MOCK_HOST,
CONF_SECRET_KEY: MOCK_SECRET_KEY,
}
@pytest.mark.usefixtures("mock_setup_entry")
async def test_already_configured(
hass: HomeAssistant,
mock_connection: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test config flow aborts when already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: MOCK_HOST},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
+69
View File
@@ -0,0 +1,69 @@
"""Tests for the Trane Local integration setup."""
from unittest.mock import MagicMock
from steamloop import AuthenticationError, SteamloopConnectionError
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_load_unload(
hass: HomeAssistant,
init_integration: MockConfigEntry,
) -> None:
"""Test loading and unloading the integration."""
entry = init_integration
assert entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_setup_connection_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setup retries on connection error."""
mock_connection.connect.side_effect = SteamloopConnectionError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_auth_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setup fails on authentication error."""
mock_connection.login.side_effect = AuthenticationError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
async def test_setup_timeout_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setup retries on timeout."""
mock_connection.connect.side_effect = TimeoutError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
+74
View File
@@ -0,0 +1,74 @@
"""Tests for the Trane Local switch platform."""
from unittest.mock import MagicMock
import pytest
from steamloop import HoldType
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_switch_entities(
hass: HomeAssistant,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Snapshot all switch entities."""
await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id)
async def test_hold_switch_off(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test hold switch reports off 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("switch.living_room_hold")
assert state is not None
assert state.state == STATE_OFF
@pytest.mark.parametrize(
("service", "expected_hold_type"),
[
(SERVICE_TURN_ON, HoldType.MANUAL),
(SERVICE_TURN_OFF, HoldType.SCHEDULE),
],
)
async def test_hold_switch_service(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
service: str,
expected_hold_type: HoldType,
) -> None:
"""Test turning on and off the hold switch."""
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{ATTR_ENTITY_ID: "switch.living_room_hold"},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1", hold_type=expected_hold_type
)