Add standby switch to MELCloud Home (#183237)

This commit is contained in:
Erwin Douna
2026-09-27 08:08:18 +02:00
committed by GitHub
parent 58e896d2ed
commit b94d64e9d3
6 changed files with 233 additions and 10 deletions
@@ -146,6 +146,9 @@
},
"overheat_protection": {
"name": "Overheat protection"
},
"standby": {
"name": "Standby"
}
}
},
@@ -30,6 +30,7 @@ class MelCloudHomeSwitchEntityDescription[_UnitT: ATAUnit | ATWUnit](
available_fn: Callable[[_UnitT], bool]
is_on_fn: Callable[[_UnitT], bool | None]
supported_fn: Callable[[_UnitT], bool] = lambda _: True
turn_on_fn: Callable[[MELCloudHome, _UnitT], Coroutine[Any, Any, None]]
turn_off_fn: Callable[[MELCloudHome, _UnitT], Coroutine[Any, Any, None]]
@@ -91,6 +92,26 @@ def _switch_descriptions[_UnitT: ATAUnit | ATWUnit](
**unit_ids(unit),
),
),
MelCloudHomeSwitchEntityDescription(
key="standby",
translation_key="standby",
device_class=SwitchDeviceClass.SWITCH,
supported_fn=lambda unit: bool(
unit.capabilities and unit.capabilities.has_standby_mode
),
available_fn=lambda _: True,
is_on_fn=lambda unit: unit.in_standby_mode,
turn_on_fn=lambda client, unit: (
client.control_ata_unit(unit.id, in_standby_mode=True)
if isinstance(unit, ATAUnit)
else client.control_atw_unit(unit.id, in_standby_mode=True)
),
turn_off_fn=lambda client, unit: (
client.control_ata_unit(unit.id, in_standby_mode=False)
if isinstance(unit, ATAUnit)
else client.control_atw_unit(unit.id, in_standby_mode=False)
),
),
)
@@ -117,11 +138,13 @@ async def async_setup_entry(
ATASwitch(coordinator, entity_description, unit)
for entity_description in ATA_SWITCHES
for unit in units
if entity_description.supported_fn(unit)
),
lambda units: (
ATWSwitch(coordinator, entity_description, unit)
for entity_description in ATW_SWITCHES
for unit in units
if entity_description.supported_fn(unit)
),
)
@@ -156,7 +179,7 @@ class ATASwitch(MelCloudHomeATAUnitEntity, SwitchEntity):
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable the protection."""
"""Turn the switch on."""
await perform_action(
self.coordinator,
self.entity_description.turn_on_fn(self.coordinator.client, self.unit),
@@ -164,7 +187,7 @@ class ATASwitch(MelCloudHomeATAUnitEntity, SwitchEntity):
@override
async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable the protection."""
"""Turn the switch off."""
await perform_action(
self.coordinator,
self.entity_description.turn_off_fn(self.coordinator.client, self.unit),
@@ -201,7 +224,7 @@ class ATWSwitch(MelCloudHomeATWUnitEntity, SwitchEntity):
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable the protection."""
"""Turn the switch on."""
await perform_action(
self.coordinator,
self.entity_description.turn_on_fn(self.coordinator.client, self.unit),
@@ -209,7 +232,7 @@ class ATWSwitch(MelCloudHomeATWUnitEntity, SwitchEntity):
@override
async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable the protection."""
"""Turn the switch off."""
await perform_action(
self.coordinator,
self.entity_description.turn_off_fn(self.coordinator.client, self.unit),
@@ -46,7 +46,7 @@
"hasOutdoorTemperatureSensor": true,
"hasVaneVertical": true,
"hasVaneHorizontal": true,
"hasStandbyMode": false
"hasStandbyMode": true
},
"frostProtection": {
"active": false,
@@ -189,7 +189,7 @@
"hasCoolingMode": true,
"minSetTankTemperature": 40,
"maxSetTankTemperature": 60,
"hasStandbyMode": false,
"hasStandbyMode": true,
"hasEnergyConsumedMeter": true
},
"rssi": -52,
@@ -37,7 +37,7 @@
'has_fan_operation_mode': None,
'has_half_degree_increments': True,
'has_outdoor_temperature_sensor': True,
'has_standby_mode': False,
'has_standby_mode': True,
'has_vane_horizontal': True,
'has_vane_vertical': True,
'max_temp_auto': 31.0,
@@ -141,7 +141,7 @@
'has_energy_consumed_meter': True,
'has_half_degrees': False,
'has_hot_water': True,
'has_standby_mode': False,
'has_standby_mode': True,
'has_zone2': True,
'max_set_tank_temperature': 60.0,
'max_set_temperature_zone1': None,
@@ -101,6 +101,57 @@
'state': 'on',
})
# ---
# name: test_all_entities[switch.heat_pump_standby-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.heat_pump_standby',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Standby',
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Standby',
'platform': 'melcloud_home',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'standby',
'unique_id': 'atw-unit-uuid-1_standby',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[switch.heat_pump_standby-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'switch',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Heat Pump Standby',
}),
'context': <ANY>,
'entity_id': 'switch.heat_pump_standby',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[switch.living_room_ac_frost_protection-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -203,3 +254,54 @@
'state': 'on',
})
# ---
# name: test_all_entities[switch.living_room_ac_standby-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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_ac_standby',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Standby',
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Standby',
'platform': 'melcloud_home',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'standby',
'unique_id': 'ata-unit-uuid-1_standby',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[switch.living_room_ac_standby-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'switch',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Room AC Standby',
}),
'context': <ANY>,
'entity_id': 'switch.living_room_ac_standby',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
+97 -2
View File
@@ -2,6 +2,7 @@
from unittest.mock import AsyncMock, patch
from aiomelcloudhome import UserContext
from aiomelcloudhome.exceptions import (
MelCloudHomeAuthenticationError,
MelCloudHomeConnectionError,
@@ -10,19 +11,24 @@ from aiomelcloudhome.exceptions import (
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.melcloud_home.const import DOMAIN
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
from tests.common import (
MockConfigEntry,
async_load_json_object_fixture,
snapshot_platform,
)
@pytest.fixture(autouse=True)
@@ -156,3 +162,92 @@ async def test_turn_on_off_exceptions(
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
async def test_no_standby_switch_without_support(
hass: HomeAssistant,
mock_melcloud_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test units without standby support get no standby switch."""
context = await async_load_json_object_fixture(hass, "context.json", DOMAIN)
building = context["buildings"][0]
building["airToAirUnits"][0]["capabilities"]["hasStandbyMode"] = False
building["airToWaterUnits"][0]["capabilities"]["hasStandbyMode"] = False
mock_melcloud_client.get_context.return_value = UserContext.model_validate(context)
await setup_integration(hass, mock_config_entry)
assert hass.states.get("switch.living_room_ac_standby") is None
assert hass.states.get("switch.heat_pump_standby") is None
@pytest.mark.parametrize(
("entity_id", "method", "unit_id"),
[
("switch.living_room_ac_standby", "control_ata_unit", "ata-unit-uuid-1"),
("switch.heat_pump_standby", "control_atw_unit", "atw-unit-uuid-1"),
],
)
@pytest.mark.parametrize("service_call", [SERVICE_TURN_ON, SERVICE_TURN_OFF])
async def test_standby_turn_on_off(
hass: HomeAssistant,
mock_melcloud_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
method: str,
unit_id: str,
service_call: str,
) -> None:
"""Test the standby switch puts the unit in or out of standby."""
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id))
assert state.state == STATE_OFF
await hass.services.async_call(
SWITCH_DOMAIN,
service_call,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
getattr(mock_melcloud_client, method).assert_called_once_with(
unit_id, in_standby_mode=service_call == SERVICE_TURN_ON
)
@pytest.mark.parametrize(
("entity_id", "method"),
[
("switch.living_room_ac_standby", "control_ata_unit"),
("switch.heat_pump_standby", "control_atw_unit"),
],
)
@pytest.mark.parametrize(
"exception",
[
MelCloudHomeAuthenticationError,
MelCloudHomeConnectionError,
MelCloudHomeTimeoutError,
],
)
async def test_standby_exceptions(
hass: HomeAssistant,
mock_melcloud_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
method: str,
exception: type[Exception],
) -> None:
"""Test the standby switch raises HomeAssistantError on client errors."""
await setup_integration(hass, mock_config_entry)
getattr(mock_melcloud_client, method).side_effect = exception
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)