From 66af6565bfda637caa6ab4db0ab6d2ba68b33254 Mon Sep 17 00:00:00 2001 From: Przemko92 <33545571+Przemko92@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:35:15 +0100 Subject: [PATCH] Add select for compit integration (#152778) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/compit/__init__.py | 1 + homeassistant/components/compit/icons.json | 105 +++++ .../components/compit/quality_scale.yaml | 5 +- homeassistant/components/compit/select.py | 432 ++++++++++++++++++ homeassistant/components/compit/strings.json | 115 +++++ tests/components/compit/__init__.py | 37 ++ tests/components/compit/conftest.py | 74 ++- .../compit/snapshots/test_select.ambr | 179 ++++++++ tests/components/compit/test_select.py | 94 ++++ 9 files changed, 1037 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/compit/icons.json create mode 100644 homeassistant/components/compit/select.py create mode 100644 tests/components/compit/snapshots/test_select.ambr create mode 100644 tests/components/compit/test_select.py diff --git a/homeassistant/components/compit/__init__.py b/homeassistant/components/compit/__init__.py index b4802181da9..cf8a22c729b 100644 --- a/homeassistant/components/compit/__init__.py +++ b/homeassistant/components/compit/__init__.py @@ -11,6 +11,7 @@ from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator PLATFORMS = [ Platform.CLIMATE, + Platform.SELECT, ] diff --git a/homeassistant/components/compit/icons.json b/homeassistant/components/compit/icons.json new file mode 100644 index 00000000000..7216dec7c0e --- /dev/null +++ b/homeassistant/components/compit/icons.json @@ -0,0 +1,105 @@ +{ + "entity": { + "select": { + "aero_by_pass": { + "default": "mdi:valve", + "state": { + "off": "mdi:valve-closed", + "on": "mdi:valve-open" + } + }, + "buffer_mode": { + "default": "mdi:database", + "state": { + "disabled": "mdi:water-boiler-off", + "schedule": "mdi:calendar-clock" + } + }, + "dhw_circulation": { + "default": "mdi:pump", + "state": { + "disabled": "mdi:pump-off", + "schedule": "mdi:calendar-clock" + } + }, + "heating_source_of_correction": { + "default": "mdi:tune-variant", + "state": { + "disabled": "mdi:cancel", + "nano_nr_1": "mdi:thermostat-box", + "nano_nr_2": "mdi:thermostat-box", + "nano_nr_3": "mdi:thermostat-box", + "nano_nr_4": "mdi:thermostat-box", + "nano_nr_5": "mdi:thermostat-box", + "no_corrections": "mdi:cancel", + "schedule": "mdi:calendar-clock", + "thermostat": "mdi:thermostat" + } + }, + "language": { + "default": "mdi:translate" + }, + "mixer_mode": { + "default": "mdi:valve", + "state": { + "disabled": "mdi:cancel", + "nano_nr_1": "mdi:thermostat-box", + "nano_nr_2": "mdi:thermostat-box", + "nano_nr_3": "mdi:thermostat-box", + "nano_nr_4": "mdi:thermostat-box", + "nano_nr_5": "mdi:thermostat-box", + "schedule": "mdi:calendar-clock", + "thermostat": "mdi:thermostat" + } + }, + "mixer_mode_zone": { + "default": "mdi:valve", + "state": { + "disabled": "mdi:cancel", + "nano_nr_1": "mdi:thermostat-box", + "nano_nr_2": "mdi:thermostat-box", + "nano_nr_3": "mdi:thermostat-box", + "nano_nr_4": "mdi:thermostat-box", + "nano_nr_5": "mdi:thermostat-box", + "schedule": "mdi:calendar-clock", + "thermostat": "mdi:thermostat" + } + }, + "nano_work_mode": { + "default": "mdi:cog-outline", + "state": { + "christmas": "mdi:pine-tree", + "manual_0": "mdi:home-floor-0", + "manual_1": "mdi:home-floor-1", + "manual_2": "mdi:home-floor-2", + "manual_3": "mdi:home-floor-3", + "out_of_home": "mdi:home-export-outline", + "schedule": "mdi:calendar-clock" + } + }, + "operating_mode": { + "default": "mdi:cog", + "state": { + "disabled": "mdi:cog-off", + "eco": "mdi:leaf" + } + }, + "solarcomp_operating_mode": { + "default": "mdi:heating-coil", + "state": { + "de_icing": "mdi:snowflake-melt", + "disabled": "mdi:cancel", + "holiday": "mdi:beach" + } + }, + "work_mode": { + "default": "mdi:cog-outline", + "state": { + "cooling": "mdi:snowflake-thermometer", + "summer": "mdi:weather-sunny", + "winter": "mdi:snowflake" + } + } + } + } +} diff --git a/homeassistant/components/compit/quality_scale.yaml b/homeassistant/components/compit/quality_scale.yaml index 88cdf4a47a4..81f8bd05ca8 100644 --- a/homeassistant/components/compit/quality_scale.yaml +++ b/homeassistant/components/compit/quality_scale.yaml @@ -73,10 +73,7 @@ rules: This integration does not have any entities that should disabled by default. entity-translations: done exception-translations: todo - icon-translations: - status: exempt - comment: | - There is no need for icon translations. + icon-translations: done reconfiguration-flow: todo repair-issues: todo stale-devices: todo diff --git a/homeassistant/components/compit/select.py b/homeassistant/components/compit/select.py new file mode 100644 index 00000000000..85dd825f3cd --- /dev/null +++ b/homeassistant/components/compit/select.py @@ -0,0 +1,432 @@ +"""Select platform for Compit integration.""" + +from dataclasses import dataclass + +from compit_inext_api.consts import CompitParameter + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER_NAME +from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class CompitDeviceDescription: + """Class to describe a Compit device.""" + + name: str + """Name of the device.""" + + parameters: dict[CompitParameter, SelectEntityDescription] + """Parameters of the device.""" + + +DESCRIPTIONS: dict[CompitParameter, SelectEntityDescription] = { + CompitParameter.LANGUAGE: SelectEntityDescription( + key=CompitParameter.LANGUAGE.value, + translation_key="language", + options=[ + "polish", + "english", + ], + ), + CompitParameter.AEROKONFBYPASS: SelectEntityDescription( + key=CompitParameter.AEROKONFBYPASS.value, + translation_key="aero_by_pass", + options=[ + "off", + "auto", + "on", + ], + ), + CompitParameter.NANO_MODE: SelectEntityDescription( + key=CompitParameter.NANO_MODE.value, + translation_key="nano_work_mode", + options=[ + "manual_3", + "manual_2", + "manual_1", + "manual_0", + "schedule", + "christmas", + "out_of_home", + ], + ), + CompitParameter.R900_OPERATING_MODE: SelectEntityDescription( + key=CompitParameter.R900_OPERATING_MODE.value, + translation_key="operating_mode", + options=[ + "disabled", + "eco", + "hybrid", + ], + ), + CompitParameter.SOLAR_COMP_OPERATING_MODE: SelectEntityDescription( + key=CompitParameter.SOLAR_COMP_OPERATING_MODE.value, + translation_key="solarcomp_operating_mode", + options=[ + "auto", + "de_icing", + "holiday", + "disabled", + ], + ), + CompitParameter.R490_OPERATING_MODE: SelectEntityDescription( + key=CompitParameter.R490_OPERATING_MODE.value, + translation_key="operating_mode", + options=[ + "disabled", + "eco", + "hybrid", + ], + ), + CompitParameter.WORK_MODE: SelectEntityDescription( + key=CompitParameter.WORK_MODE.value, + translation_key="work_mode", + options=[ + "winter", + "summer", + "cooling", + ], + ), + CompitParameter.R470_OPERATING_MODE: SelectEntityDescription( + key=CompitParameter.R470_OPERATING_MODE.value, + translation_key="operating_mode", + options=[ + "disabled", + "auto", + "eco", + ], + ), + CompitParameter.HEATING_SOURCE_OF_CORRECTION: SelectEntityDescription( + key=CompitParameter.HEATING_SOURCE_OF_CORRECTION.value, + translation_key="heating_source_of_correction", + options=[ + "no_corrections", + "schedule", + "thermostat", + "nano_nr_1", + "nano_nr_2", + "nano_nr_3", + "nano_nr_4", + "nano_nr_5", + ], + ), + CompitParameter.BIOMAX_MIXER_MODE_ZONE_1: SelectEntityDescription( + key=CompitParameter.BIOMAX_MIXER_MODE_ZONE_1.value, + translation_key="mixer_mode_zone", + options=[ + "disabled", + "without_thermostat", + "schedule", + "thermostat", + "nano_nr_1", + "nano_nr_2", + "nano_nr_3", + "nano_nr_4", + "nano_nr_5", + ], + translation_placeholders={"zone": "1"}, + ), + CompitParameter.BIOMAX_MIXER_MODE_ZONE_2: SelectEntityDescription( + key=CompitParameter.BIOMAX_MIXER_MODE_ZONE_2.value, + translation_key="mixer_mode_zone", + options=[ + "disabled", + "without_thermostat", + "schedule", + "thermostat", + "nano_nr_1", + "nano_nr_2", + "nano_nr_3", + "nano_nr_4", + "nano_nr_5", + ], + translation_placeholders={"zone": "2"}, + ), + CompitParameter.DHW_CIRCULATION_MODE: SelectEntityDescription( + key=CompitParameter.DHW_CIRCULATION_MODE.value, + translation_key="dhw_circulation", + options=[ + "disabled", + "constant", + "schedule", + ], + ), + CompitParameter.BIOMAX_HEATING_SOURCE_OF_CORRECTION: SelectEntityDescription( + key=CompitParameter.BIOMAX_HEATING_SOURCE_OF_CORRECTION.value, + translation_key="heating_source_of_correction", + options=[ + "disabled", + "no_corrections", + "schedule", + "thermostat", + "nano_nr_1", + "nano_nr_2", + "nano_nr_3", + "nano_nr_4", + "nano_nr_5", + ], + ), + CompitParameter.MIXER_MODE: SelectEntityDescription( + key=CompitParameter.MIXER_MODE.value, + translation_key="mixer_mode", + options=[ + "no_corrections", + "schedule", + "thermostat", + "nano_nr_1", + "nano_nr_2", + "nano_nr_3", + "nano_nr_4", + "nano_nr_5", + ], + ), + CompitParameter.R480_OPERATING_MODE: SelectEntityDescription( + key=CompitParameter.R480_OPERATING_MODE.value, + translation_key="operating_mode", + options=[ + "disabled", + "eco", + "hybrid", + ], + ), + CompitParameter.BUFFER_MODE: SelectEntityDescription( + key=CompitParameter.BUFFER_MODE.value, + translation_key="buffer_mode", + options=[ + "schedule", + "manual", + "disabled", + ], + ), +} + + +DEVICE_DEFINITIONS: dict[int, CompitDeviceDescription] = { + 223: CompitDeviceDescription( + name="Nano Color 2", + parameters={ + CompitParameter.LANGUAGE: DESCRIPTIONS[CompitParameter.LANGUAGE], + CompitParameter.AEROKONFBYPASS: DESCRIPTIONS[ + CompitParameter.AEROKONFBYPASS + ], + }, + ), + 12: CompitDeviceDescription( + name="Nano Color", + parameters={ + CompitParameter.LANGUAGE: DESCRIPTIONS[CompitParameter.LANGUAGE], + CompitParameter.AEROKONFBYPASS: DESCRIPTIONS[ + CompitParameter.AEROKONFBYPASS + ], + }, + ), + 7: CompitDeviceDescription( + name="Nano One", + parameters={ + CompitParameter.LANGUAGE: DESCRIPTIONS[CompitParameter.LANGUAGE], + CompitParameter.NANO_MODE: DESCRIPTIONS[CompitParameter.NANO_MODE], + }, + ), + 224: CompitDeviceDescription( + name="R 900", + parameters={ + CompitParameter.R900_OPERATING_MODE: DESCRIPTIONS[ + CompitParameter.R900_OPERATING_MODE + ], + }, + ), + 45: CompitDeviceDescription( + name="SolarComp971", + parameters={ + CompitParameter.SOLAR_COMP_OPERATING_MODE: DESCRIPTIONS[ + CompitParameter.SOLAR_COMP_OPERATING_MODE + ], + }, + ), + 99: CompitDeviceDescription( + name="SolarComp971C", + parameters={ + CompitParameter.SOLAR_COMP_OPERATING_MODE: DESCRIPTIONS[ + CompitParameter.SOLAR_COMP_OPERATING_MODE + ], + }, + ), + 44: CompitDeviceDescription( + name="SolarComp 951", + parameters={ + CompitParameter.SOLAR_COMP_OPERATING_MODE: DESCRIPTIONS[ + CompitParameter.SOLAR_COMP_OPERATING_MODE + ], + }, + ), + 92: CompitDeviceDescription( + name="r490", + parameters={ + CompitParameter.R490_OPERATING_MODE: DESCRIPTIONS[ + CompitParameter.R490_OPERATING_MODE + ], + CompitParameter.WORK_MODE: DESCRIPTIONS[CompitParameter.WORK_MODE], + }, + ), + 34: CompitDeviceDescription( + name="r470", + parameters={ + CompitParameter.R470_OPERATING_MODE: DESCRIPTIONS[ + CompitParameter.R470_OPERATING_MODE + ], + CompitParameter.HEATING_SOURCE_OF_CORRECTION: DESCRIPTIONS[ + CompitParameter.HEATING_SOURCE_OF_CORRECTION + ], + }, + ), + 201: CompitDeviceDescription( + name="BioMax775", + parameters={ + CompitParameter.BIOMAX_MIXER_MODE_ZONE_1: DESCRIPTIONS[ + CompitParameter.BIOMAX_MIXER_MODE_ZONE_1 + ], + CompitParameter.BIOMAX_MIXER_MODE_ZONE_2: DESCRIPTIONS[ + CompitParameter.BIOMAX_MIXER_MODE_ZONE_2 + ], + CompitParameter.DHW_CIRCULATION_MODE: DESCRIPTIONS[ + CompitParameter.DHW_CIRCULATION_MODE + ], + }, + ), + 36: CompitDeviceDescription( + name="BioMax742", + parameters={ + CompitParameter.BIOMAX_HEATING_SOURCE_OF_CORRECTION: DESCRIPTIONS[ + CompitParameter.BIOMAX_HEATING_SOURCE_OF_CORRECTION + ], + CompitParameter.BIOMAX_MIXER_MODE_ZONE_1: DESCRIPTIONS[ + CompitParameter.BIOMAX_MIXER_MODE_ZONE_1 + ], + CompitParameter.DHW_CIRCULATION_MODE: DESCRIPTIONS[ + CompitParameter.DHW_CIRCULATION_MODE + ], + }, + ), + 75: CompitDeviceDescription( + name="BioMax772", + parameters={ + CompitParameter.BIOMAX_MIXER_MODE_ZONE_1: DESCRIPTIONS[ + CompitParameter.BIOMAX_MIXER_MODE_ZONE_1 + ], + CompitParameter.BIOMAX_MIXER_MODE_ZONE_2: DESCRIPTIONS[ + CompitParameter.BIOMAX_MIXER_MODE_ZONE_2 + ], + CompitParameter.DHW_CIRCULATION_MODE: DESCRIPTIONS[ + CompitParameter.DHW_CIRCULATION_MODE + ], + }, + ), + 5: CompitDeviceDescription( + name="R350 T3", + parameters={ + CompitParameter.MIXER_MODE: DESCRIPTIONS[CompitParameter.MIXER_MODE], + }, + ), + 215: CompitDeviceDescription( + name="R480", + parameters={ + CompitParameter.R480_OPERATING_MODE: DESCRIPTIONS[ + CompitParameter.R480_OPERATING_MODE + ], + CompitParameter.BUFFER_MODE: DESCRIPTIONS[CompitParameter.BUFFER_MODE], + }, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: CompitConfigEntry, + async_add_devices: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Compit select entities from a config entry.""" + + coordinator = entry.runtime_data + select_entities = [] + for device_id, device in coordinator.connector.all_devices.items(): + device_definition = DEVICE_DEFINITIONS.get(device.definition.code) + + if not device_definition: + continue + + for code, entity_description in device_definition.parameters.items(): + param = next( + (p for p in device.state.params if p.code == entity_description.key), + None, + ) + + if param is None: + continue + + select_entities.append( + CompitSelect( + coordinator, + device_id, + device_definition.name, + code, + entity_description, + ) + ) + + async_add_devices(select_entities) + + +class CompitSelect(CoordinatorEntity[CompitDataUpdateCoordinator], SelectEntity): + """Representation of a Compit select entity.""" + + def __init__( + self, + coordinator: CompitDataUpdateCoordinator, + device_id: int, + device_name: str, + parameter_code: CompitParameter, + entity_description: SelectEntityDescription, + ) -> None: + """Initialize the select entity.""" + super().__init__(coordinator) + self.device_id = device_id + self.entity_description = entity_description + self._attr_has_entity_name = True + self._attr_unique_id = f"{device_id}_{entity_description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(device_id))}, + name=device_name, + manufacturer=MANUFACTURER_NAME, + model=device_name, + ) + self.parameter_code = parameter_code + + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + super().available + and self.coordinator.connector.get_device(self.device_id) is not None + ) + + @property + def current_option(self) -> str | None: + """Return the current option.""" + return self.coordinator.connector.get_current_option( + self.device_id, self.parameter_code + ) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.coordinator.connector.select_device_option( + self.device_id, self.parameter_code, option + ) + self.async_write_ha_state() diff --git a/homeassistant/components/compit/strings.json b/homeassistant/components/compit/strings.json index b5c6c4b419c..73d456c191d 100644 --- a/homeassistant/components/compit/strings.json +++ b/homeassistant/components/compit/strings.json @@ -31,5 +31,120 @@ "title": "Connect to Compit iNext" } } + }, + "entity": { + "select": { + "aero_by_pass": { + "name": "Bypass", + "state": { + "auto": "[%key:common::state::auto%]", + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + }, + "buffer_mode": { + "name": "Buffer mode", + "state": { + "disabled": "[%key:common::state::disabled%]", + "manual": "[%key:common::state::manual%]", + "schedule": "Schedule" + } + }, + "dhw_circulation": { + "name": "Domestic hot water circulation", + "state": { + "constant": "Constant", + "disabled": "[%key:common::state::disabled%]", + "schedule": "Schedule" + } + }, + "heating_source_of_correction": { + "name": "Heating source of correction", + "state": { + "disabled": "[%key:common::state::disabled%]", + "nano_nr_1": "Nano 1", + "nano_nr_2": "Nano 2", + "nano_nr_3": "Nano 3", + "nano_nr_4": "Nano 4", + "nano_nr_5": "Nano 5", + "no_corrections": "No corrections", + "schedule": "Schedule", + "thermostat": "Thermostat" + } + }, + "language": { + "name": "Language", + "state": { + "english": "English", + "polish": "Polish" + } + }, + "mixer_mode": { + "name": "Mixer mode", + "state": { + "nano_nr_1": "Nano 1", + "nano_nr_2": "Nano 2", + "nano_nr_3": "Nano 3", + "nano_nr_4": "Nano 4", + "nano_nr_5": "Nano 5", + "no_corrections": "No corrections", + "schedule": "Schedule", + "thermostat": "Thermostat" + } + }, + "mixer_mode_zone": { + "name": "Zone {zone} mixer mode", + "state": { + "disabled": "[%key:common::state::disabled%]", + "nano_nr_1": "Nano 1", + "nano_nr_2": "Nano 2", + "nano_nr_3": "Nano 3", + "nano_nr_4": "Nano 4", + "nano_nr_5": "Nano 5", + "no_corrections": "No corrections", + "schedule": "Schedule", + "thermostat": "Thermostat", + "without_thermostat": "Without thermostat" + } + }, + "nano_work_mode": { + "name": "Nano work mode", + "state": { + "christmas": "Christmas", + "manual_0": "Manual 0", + "manual_1": "Manual 1", + "manual_2": "Manual 2", + "manual_3": "Manual 3", + "out_of_home": "Out of home", + "schedule": "Schedule" + } + }, + "operating_mode": { + "name": "Operating mode", + "state": { + "auto": "[%key:common::state::auto%]", + "disabled": "[%key:common::state::disabled%]", + "eco": "Eco", + "hybrid": "Hybrid" + } + }, + "solarcomp_operating_mode": { + "name": "Operating mode", + "state": { + "auto": "[%key:common::state::auto%]", + "de_icing": "De-icing", + "disabled": "[%key:common::state::disabled%]", + "holiday": "Holiday" + } + }, + "work_mode": { + "name": "Current season", + "state": { + "cooling": "Cooling", + "summer": "Summer", + "winter": "Winter" + } + } + } } } diff --git a/tests/components/compit/__init__.py b/tests/components/compit/__init__.py index a817df77ad0..a4306252a58 100644 --- a/tests/components/compit/__init__.py +++ b/tests/components/compit/__init__.py @@ -1 +1,38 @@ """Tests for the compit component.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the Compit 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() + + +def snapshot_compit_entities( + hass: HomeAssistant, + entity_registry: EntityRegistry, + snapshot: SnapshotAssertion, + platform: Platform, +) -> None: + """Snapshot Compit entities.""" + entities = sorted( + hass.states.async_all(platform), + key=lambda state: state.entity_id, + ) + for entity_state in entities: + entity_entry = entity_registry.async_get(entity_state.entity_id) + assert entity_entry and entity_entry == snapshot( + name=f"{entity_entry.entity_id}-entry" + ) + assert entity_state == snapshot(name=f"{entity_entry.entity_id}-state") diff --git a/tests/components/compit/conftest.py b/tests/components/compit/conftest.py index e8e4b09d9be..60ad60003c7 100644 --- a/tests/components/compit/conftest.py +++ b/tests/components/compit/conftest.py @@ -1,8 +1,9 @@ """Common fixtures for the Compit tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch +from compit_inext_api import CompitParameter import pytest from homeassistant.components.compit.const import DOMAIN @@ -39,3 +40,74 @@ def mock_compit_api() -> Generator[AsyncMock]: "homeassistant.components.compit.config_flow.CompitApiConnector.init", ) as mock_api: yield mock_api + + +@pytest.fixture +def mock_connector(): + """Mock CompitApiConnector devices.""" + + mock_device_1 = MagicMock() + mock_device_1.definition.name = "Test Device 1" + mock_device_1.state.params = [ + MagicMock(code="__tr_pracy_pc", value="eco"), + MagicMock( + code="__trybpracy", value="de_icing" + ), # parameter not relevant for this device, should be ignored + ] + mock_device_1.definition.code = 224 # R 900 + + mock_device_2 = MagicMock() + mock_device_2.state.params = [ + MagicMock(code="_jezyk", value="english"), + MagicMock(code="__aerokonfbypass", value="off"), + ] + mock_device_2.definition.code = 223 # Nano Color 2 + + mock_device_3 = MagicMock() + mock_device_3.definition.code = 999 # Unknown Device + mock_device_3.state = None + + all_devices = {1: mock_device_1, 2: mock_device_2, 3: mock_device_3} + + def mock_get_device(device_id: int): + return all_devices.get(device_id) + + def get_current_option(device_id: int, parameter_code: CompitParameter): + return next( + ( + p + for p in all_devices[device_id].state.params + if p.code == parameter_code.value + ), + None, + ).value + + def select_device_option( + device_id: int, parameter_code: CompitParameter, value: str + ): + next( + p + for p in all_devices[device_id].state.params + if p.code == parameter_code.value + ).value = value + return True + + mock_instance = MagicMock() + mock_instance.init = AsyncMock(return_value=True) + mock_instance.all_devices = all_devices + mock_instance.get_current_option = MagicMock(side_effect=get_current_option) + mock_instance.select_device_option = AsyncMock(side_effect=select_device_option) + mock_instance.update_state = AsyncMock() + mock_instance.get_device = MagicMock(side_effect=mock_get_device) + + with ( + patch( + "homeassistant.components.compit.CompitApiConnector", + return_value=mock_instance, + ), + patch( + "homeassistant.components.compit.coordinator.CompitApiConnector", + return_value=mock_instance, + ), + ): + yield mock_instance diff --git a/tests/components/compit/snapshots/test_select.ambr b/tests/components/compit/snapshots/test_select.ambr new file mode 100644 index 00000000000..115aa9f9045 --- /dev/null +++ b/tests/components/compit/snapshots/test_select.ambr @@ -0,0 +1,179 @@ +# serializer version: 1 +# name: test_select_entities_snapshot[select.nano_color_2_bypass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'auto', + 'on', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.nano_color_2_bypass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Bypass', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bypass', + 'platform': 'compit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'aero_by_pass', + 'unique_id': '2___aerokonfbypass', + 'unit_of_measurement': None, + }) +# --- +# name: test_select_entities_snapshot[select.nano_color_2_bypass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nano Color 2 Bypass', + 'options': list([ + 'off', + 'auto', + 'on', + ]), + }), + 'context': , + 'entity_id': 'select.nano_color_2_bypass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_select_entities_snapshot[select.nano_color_2_language-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'polish', + 'english', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.nano_color_2_language', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Language', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Language', + 'platform': 'compit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'language', + 'unique_id': '2__jezyk', + 'unit_of_measurement': None, + }) +# --- +# name: test_select_entities_snapshot[select.nano_color_2_language-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Nano Color 2 Language', + 'options': list([ + 'polish', + 'english', + ]), + }), + 'context': , + 'entity_id': 'select.nano_color_2_language', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'english', + }) +# --- +# name: test_select_entities_snapshot[select.r_900_operating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'disabled', + 'eco', + 'hybrid', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.r_900_operating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Operating mode', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Operating mode', + 'platform': 'compit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'operating_mode', + 'unique_id': '1___tr_pracy_pc', + 'unit_of_measurement': None, + }) +# --- +# name: test_select_entities_snapshot[select.r_900_operating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'R 900 Operating mode', + 'options': list([ + 'disabled', + 'eco', + 'hybrid', + ]), + }), + 'context': , + 'entity_id': 'select.r_900_operating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'eco', + }) +# --- diff --git a/tests/components/compit/test_select.py b/tests/components/compit/test_select.py new file mode 100644 index 00000000000..c0d6e63318a --- /dev/null +++ b/tests/components/compit/test_select.py @@ -0,0 +1,94 @@ +"""Tests for the Compit select platform.""" + +from typing import Any +from unittest.mock import MagicMock + +from compit_inext_api.consts import CompitParameter +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration, snapshot_compit_entities + +from tests.common import MockConfigEntry + + +async def test_select_entities_snapshot( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_connector: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Snapshot test for select entities creation, unique IDs, and device info.""" + await setup_integration(hass, mock_config_entry) + + snapshot_compit_entities(hass, entity_registry, snapshot, Platform.SELECT) + + +@pytest.mark.parametrize( + "mock_return_value", + [ + None, + 1, + "invalid", + ], +) +async def test_select_unknown_device_parameters( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_connector: MagicMock, + mock_return_value: Any, +) -> None: + """Test that select entity shows unknown when get_current_option returns various invalid values.""" + mock_connector.get_current_option.side_effect = ( + lambda device_id, parameter_code: mock_return_value + ) + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("select.nano_color_2_language") + assert state is not None + assert state.state == "unknown" + + +async def test_select_option( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_connector: MagicMock +) -> None: + """Test selecting an option.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.nano_color_2_language", "option": "polish"}, + blocking=True, + ) + + mock_connector.select_device_option.assert_called_once() + assert mock_connector.get_current_option(2, CompitParameter.LANGUAGE) == "polish" + + +async def test_select_invalid_option( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_connector: MagicMock +) -> None: + """Test selecting an invalid option.""" + + await setup_integration(hass, mock_config_entry) + + with pytest.raises( + ServiceValidationError, + match=r"Option invalid is not valid for entity select\.nano_color_2_language", + ): + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.nano_color_2_language", "option": "invalid"}, + blocking=True, + ) + + mock_connector.select_device_option.assert_not_called()