mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Add IntelliClima Select platform (#163637)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Norbert Rittel <norbert@rittel.de> Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
co-authored by
Copilot
Norbert Rittel
Joostlek
parent
dc5eab6810
commit
501e095578
@@ -9,7 +9,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .const import LOGGER
|
||||
from .coordinator import IntelliClimaConfigEntry, IntelliClimaCoordinator
|
||||
|
||||
PLATFORMS = [Platform.FAN]
|
||||
PLATFORMS = [Platform.FAN, Platform.SELECT]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -27,8 +27,6 @@ class IntelliClimaEntity(CoordinatorEntity[IntelliClimaCoordinator]):
|
||||
"""Class initializer."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
|
||||
self._attr_unique_id = device.id
|
||||
|
||||
# Make this HA "device" use the IntelliClima device name.
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.id)},
|
||||
|
||||
@@ -62,6 +62,7 @@ class IntelliClimaVMCFan(IntelliClimaECOEntity, FanEntity):
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
self._speed_range = (int(FanSpeed.sleep), int(FanSpeed.high))
|
||||
self._attr_unique_id = device.id
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -49,7 +49,7 @@ rules:
|
||||
comment: |
|
||||
Unclear if discovery is possible.
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Select platform for IntelliClima VMC."""
|
||||
|
||||
from pyintelliclima.const import FanMode, FanSpeed
|
||||
from pyintelliclima.intelliclima_types import IntelliClimaECO
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import IntelliClimaConfigEntry, IntelliClimaCoordinator
|
||||
from .entity import IntelliClimaECOEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
FAN_MODE_TO_INTELLICLIMA_MODE = {
|
||||
"forward": FanMode.inward,
|
||||
"reverse": FanMode.outward,
|
||||
"alternate": FanMode.alternate,
|
||||
"sensor": FanMode.sensor,
|
||||
}
|
||||
INTELLICLIMA_MODE_TO_FAN_MODE = {v: k for k, v in FAN_MODE_TO_INTELLICLIMA_MODE.items()}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: IntelliClimaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up IntelliClima VMC fan mode select."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities: list[IntelliClimaVMCFanModeSelect] = [
|
||||
IntelliClimaVMCFanModeSelect(
|
||||
coordinator=coordinator,
|
||||
device=ecocomfort2,
|
||||
)
|
||||
for ecocomfort2 in coordinator.data.ecocomfort2_devices.values()
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class IntelliClimaVMCFanModeSelect(IntelliClimaECOEntity, SelectEntity):
|
||||
"""Representation of an IntelliClima VMC fan mode selector."""
|
||||
|
||||
_attr_translation_key = "fan_mode"
|
||||
_attr_options = ["forward", "reverse", "alternate", "sensor"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: IntelliClimaCoordinator,
|
||||
device: IntelliClimaECO,
|
||||
) -> None:
|
||||
"""Class initializer."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
self._attr_unique_id = f"{device.id}_fan_mode"
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current fan mode."""
|
||||
device_data = self._device_data
|
||||
|
||||
if device_data.mode_set == FanMode.off:
|
||||
return None
|
||||
|
||||
# If in auto mode (sensor mode with auto speed), return None (handled by fan entity preset mode)
|
||||
if (
|
||||
device_data.speed_set == FanSpeed.auto
|
||||
and device_data.mode_set == FanMode.sensor
|
||||
):
|
||||
return None
|
||||
|
||||
return INTELLICLIMA_MODE_TO_FAN_MODE.get(FanMode(device_data.mode_set))
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the fan mode."""
|
||||
device_data = self._device_data
|
||||
|
||||
mode = FAN_MODE_TO_INTELLICLIMA_MODE[option]
|
||||
|
||||
# Determine speed: keep current speed if available, otherwise default to sleep
|
||||
if (
|
||||
device_data.speed_set == FanSpeed.auto
|
||||
or device_data.mode_set == FanMode.off
|
||||
):
|
||||
speed = FanSpeed.sleep
|
||||
else:
|
||||
speed = device_data.speed_set
|
||||
|
||||
await self.coordinator.api.ecocomfort.set_mode_speed(
|
||||
self._device_sn, mode, speed
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -22,5 +22,18 @@
|
||||
"description": "Authenticate against IntelliClima cloud"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"select": {
|
||||
"fan_mode": {
|
||||
"name": "Fan direction mode",
|
||||
"state": {
|
||||
"alternate": "Alternating",
|
||||
"forward": "Forward",
|
||||
"reverse": "Reverse",
|
||||
"sensor": "Sensor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_select_entities.2
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
tuple(
|
||||
'bluetooth',
|
||||
'00:11:22:33:44:55',
|
||||
),
|
||||
tuple(
|
||||
'mac',
|
||||
'00:11:22:33:44:55',
|
||||
),
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'intelliclima',
|
||||
'56789',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Fantini Cosmi',
|
||||
'model': 'ECOCOMFORT 2.0',
|
||||
'model_id': None,
|
||||
'name': 'Test VMC',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': '11223344',
|
||||
'sw_version': '0.6.8',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_select_entities[select.test_vmc_fan_direction_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'forward',
|
||||
'reverse',
|
||||
'alternate',
|
||||
'sensor',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.test_vmc_fan_direction_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Fan direction mode',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Fan direction mode',
|
||||
'platform': 'intelliclima',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'fan_mode',
|
||||
'unique_id': '56789_fan_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_select_entities[select.test_vmc_fan_direction_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test VMC Fan direction mode',
|
||||
'options': list([
|
||||
'forward',
|
||||
'reverse',
|
||||
'alternate',
|
||||
'sensor',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.test_vmc_fan_direction_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'forward',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Test IntelliClima Select."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pyintelliclima.const import FanMode, FanSpeed
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.select import (
|
||||
ATTR_OPTION,
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
SELECT_ENTITY_ID = "select.test_vmc_fan_direction_mode"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_intelliclima_select_only(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> AsyncGenerator[None]:
|
||||
"""Set up IntelliClima integration with only the select platform."""
|
||||
with (
|
||||
patch("homeassistant.components.intelliclima.PLATFORMS", [Platform.SELECT]),
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
# Let tests run against this initialized state
|
||||
yield
|
||||
|
||||
|
||||
async def test_all_select_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
# There should be exactly one select entity
|
||||
select_entries = [
|
||||
entry
|
||||
for entry in entity_registry.entities.values()
|
||||
if entry.platform == "intelliclima" and entry.domain == SELECT_DOMAIN
|
||||
]
|
||||
assert len(select_entries) == 1
|
||||
|
||||
entity_entry = select_entries[0]
|
||||
assert entity_entry.device_id
|
||||
assert (device_entry := device_registry.async_get(entity_entry.device_id))
|
||||
assert device_entry == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("option", "expected_mode"),
|
||||
[
|
||||
("forward", FanMode.inward),
|
||||
("reverse", FanMode.outward),
|
||||
("alternate", FanMode.alternate),
|
||||
("sensor", FanMode.sensor),
|
||||
],
|
||||
)
|
||||
async def test_select_option_keeps_current_speed(
|
||||
hass: HomeAssistant,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
option: str,
|
||||
expected_mode: FanMode,
|
||||
) -> None:
|
||||
"""Selecting any valid option retains the current speed and calls set_mode_speed."""
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: SELECT_ENTITY_ID, ATTR_OPTION: option},
|
||||
blocking=True,
|
||||
)
|
||||
# Device starts with speed_set="3" (from single_eco_device in conftest),
|
||||
# mode is not off and not auto, so current speed is preserved.
|
||||
mock_cloud_interface.ecocomfort.set_mode_speed.assert_awaited_once_with(
|
||||
"11223344", expected_mode, "3"
|
||||
)
|
||||
|
||||
|
||||
async def test_select_option_when_off_defaults_speed_to_sleep(
|
||||
hass: HomeAssistant,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
single_eco_device,
|
||||
) -> None:
|
||||
"""When the device is off, selecting an option defaults the speed to FanSpeed.sleep."""
|
||||
# Mutate the shared fixture object – coordinator.data points to the same reference.
|
||||
eco = list(single_eco_device.ecocomfort2_devices.values())[0]
|
||||
eco.mode_set = FanMode.off
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: SELECT_ENTITY_ID, ATTR_OPTION: "forward"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_cloud_interface.ecocomfort.set_mode_speed.assert_awaited_once_with(
|
||||
"11223344", FanMode.inward, FanSpeed.sleep
|
||||
)
|
||||
|
||||
|
||||
async def test_select_option_in_auto_mode_defaults_speed_to_sleep(
|
||||
hass: HomeAssistant,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
single_eco_device,
|
||||
) -> None:
|
||||
"""When speed_set is FanSpeed.auto (auto preset), selecting an option defaults to sleep speed."""
|
||||
eco = list(single_eco_device.ecocomfort2_devices.values())[0]
|
||||
eco.speed_set = FanSpeed.auto
|
||||
eco.mode_set = FanMode.sensor
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: SELECT_ENTITY_ID, ATTR_OPTION: "reverse"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_cloud_interface.ecocomfort.set_mode_speed.assert_awaited_once_with(
|
||||
"11223344", FanMode.outward, FanSpeed.sleep
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("option", ["forward", "reverse", "alternate", "sensor"])
|
||||
async def test_select_option_does_not_call_turn_off(
|
||||
hass: HomeAssistant,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
option: str,
|
||||
) -> None:
|
||||
"""Selecting an option should never call turn_off."""
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: SELECT_ENTITY_ID, ATTR_OPTION: option},
|
||||
blocking=True,
|
||||
)
|
||||
mock_cloud_interface.ecocomfort.turn_off.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_select_option_triggers_coordinator_refresh(
|
||||
hass: HomeAssistant,
|
||||
mock_cloud_interface: AsyncMock,
|
||||
) -> None:
|
||||
"""Selecting an option should trigger a coordinator refresh after the API call."""
|
||||
initial_call_count = mock_cloud_interface.get_all_device_status.call_count
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: SELECT_ENTITY_ID, ATTR_OPTION: "sensor"},
|
||||
blocking=True,
|
||||
)
|
||||
# A refresh must have been requested, so the status fetch count increases.
|
||||
assert mock_cloud_interface.get_all_device_status.call_count > initial_call_count
|
||||
Reference in New Issue
Block a user