diff --git a/homeassistant/components/iron_os/__init__.py b/homeassistant/components/iron_os/__init__.py index 0fe5acc2db6..9655f7bfcdd 100644 --- a/homeassistant/components/iron_os/__init__.py +++ b/homeassistant/components/iron_os/__init__.py @@ -28,6 +28,7 @@ from .coordinator import ( PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.UPDATE, ] diff --git a/homeassistant/components/iron_os/icons.json b/homeassistant/components/iron_os/icons.json index eadcc17bb37..0d26b027c3f 100644 --- a/homeassistant/components/iron_os/icons.json +++ b/homeassistant/components/iron_os/icons.json @@ -63,13 +63,39 @@ "min_voltage_per_cell": { "default": "mdi:fuel-cell" }, - "min_dc_voltage_cells": { - "default": "mdi:battery-arrow-down" - }, "power_limit": { "default": "mdi:flash-alert" } }, + "select": { + "locking_mode": { + "default": "mdi:download-lock" + }, + "orientation_mode": { + "default": "mdi:screen-rotation" + }, + "autostart_mode": { + "default": "mdi:power-standby" + }, + "animation_speed": { + "default": "mdi:image-refresh" + }, + "min_dc_voltage_cells": { + "default": "mdi:fuel-cell" + }, + "temp_unit": { + "default": "mdi:temperature-celsius", + "state": { + "fahrenheit": "mdi:temperature-fahrenheit" + } + }, + "desc_scroll_speed": { + "default": "mdi:message-text-fast" + }, + "logo_duration": { + "default": "mdi:clock-digital" + } + }, "sensor": { "live_temperature": { "default": "mdi:soldering-iron" diff --git a/homeassistant/components/iron_os/select.py b/homeassistant/components/iron_os/select.py new file mode 100644 index 00000000000..c863e076f0b --- /dev/null +++ b/homeassistant/components/iron_os/select.py @@ -0,0 +1,208 @@ +"""Select platform for IronOS integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import Enum, StrEnum +from typing import Any + +from pynecil import ( + AnimationSpeed, + AutostartMode, + BatteryType, + CharSetting, + CommunicationError, + LockingMode, + LogoDuration, + ScreenOrientationMode, + ScrollSpeed, + SettingsDataResponse, + TempUnit, +) + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import IronOSConfigEntry +from .const import DOMAIN +from .coordinator import IronOSCoordinators +from .entity import IronOSBaseEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class IronOSSelectEntityDescription(SelectEntityDescription): + """Describes IronOS select entity.""" + + value_fn: Callable[[SettingsDataResponse], str | None] + characteristic: CharSetting + raw_value_fn: Callable[[str], Any] | None = None + + +class PinecilSelect(StrEnum): + """Select controls for Pinecil device.""" + + MIN_DC_VOLTAGE_CELLS = "min_dc_voltage_cells" + ORIENTATION_MODE = "orientation_mode" + ANIMATION_SPEED = "animation_speed" + AUTOSTART_MODE = "autostart_mode" + TEMP_UNIT = "temp_unit" + DESC_SCROLL_SPEED = "desc_scroll_speed" + LOCKING_MODE = "locking_mode" + LOGO_DURATION = "logo_duration" + + +def enum_to_str(enum: Enum | None) -> str | None: + """Convert enum name to lower-case string.""" + return enum.name.lower() if isinstance(enum, Enum) else None + + +PINECIL_SELECT_DESCRIPTIONS: tuple[IronOSSelectEntityDescription, ...] = ( + IronOSSelectEntityDescription( + key=PinecilSelect.MIN_DC_VOLTAGE_CELLS, + translation_key=PinecilSelect.MIN_DC_VOLTAGE_CELLS, + characteristic=CharSetting.MIN_DC_VOLTAGE_CELLS, + value_fn=lambda x: enum_to_str(x.get("min_dc_voltage_cells")), + raw_value_fn=lambda value: BatteryType[value.upper()], + options=[x.name.lower() for x in BatteryType], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + IronOSSelectEntityDescription( + key=PinecilSelect.ORIENTATION_MODE, + translation_key=PinecilSelect.ORIENTATION_MODE, + characteristic=CharSetting.ORIENTATION_MODE, + value_fn=lambda x: enum_to_str(x.get("orientation_mode")), + raw_value_fn=lambda value: ScreenOrientationMode[value.upper()], + options=[x.name.lower() for x in ScreenOrientationMode], + entity_category=EntityCategory.CONFIG, + ), + IronOSSelectEntityDescription( + key=PinecilSelect.ANIMATION_SPEED, + translation_key=PinecilSelect.ANIMATION_SPEED, + characteristic=CharSetting.ANIMATION_SPEED, + value_fn=lambda x: enum_to_str(x.get("animation_speed")), + raw_value_fn=lambda value: AnimationSpeed[value.upper()], + options=[x.name.lower() for x in AnimationSpeed], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + IronOSSelectEntityDescription( + key=PinecilSelect.AUTOSTART_MODE, + translation_key=PinecilSelect.AUTOSTART_MODE, + characteristic=CharSetting.AUTOSTART_MODE, + value_fn=lambda x: enum_to_str(x.get("autostart_mode")), + raw_value_fn=lambda value: AutostartMode[value.upper()], + options=[x.name.lower() for x in AutostartMode], + entity_category=EntityCategory.CONFIG, + ), + IronOSSelectEntityDescription( + key=PinecilSelect.TEMP_UNIT, + translation_key=PinecilSelect.TEMP_UNIT, + characteristic=CharSetting.TEMP_UNIT, + value_fn=lambda x: enum_to_str(x.get("temp_unit")), + raw_value_fn=lambda value: TempUnit[value.upper()], + options=[x.name.lower() for x in TempUnit], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + IronOSSelectEntityDescription( + key=PinecilSelect.DESC_SCROLL_SPEED, + translation_key=PinecilSelect.DESC_SCROLL_SPEED, + characteristic=CharSetting.DESC_SCROLL_SPEED, + value_fn=lambda x: enum_to_str(x.get("desc_scroll_speed")), + raw_value_fn=lambda value: ScrollSpeed[value.upper()], + options=[x.name.lower() for x in ScrollSpeed], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + IronOSSelectEntityDescription( + key=PinecilSelect.LOCKING_MODE, + translation_key=PinecilSelect.LOCKING_MODE, + characteristic=CharSetting.LOCKING_MODE, + value_fn=lambda x: enum_to_str(x.get("locking_mode")), + raw_value_fn=lambda value: LockingMode[value.upper()], + options=[x.name.lower() for x in LockingMode], + entity_category=EntityCategory.CONFIG, + ), + IronOSSelectEntityDescription( + key=PinecilSelect.LOGO_DURATION, + translation_key=PinecilSelect.LOGO_DURATION, + characteristic=CharSetting.LOGO_DURATION, + value_fn=lambda x: enum_to_str(x.get("logo_duration")), + raw_value_fn=lambda value: LogoDuration[value.upper()], + options=[x.name.lower() for x in LogoDuration], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: IronOSConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up select entities from a config entry.""" + coordinator = entry.runtime_data + + async_add_entities( + IronOSSelectEntity(coordinator, description) + for description in PINECIL_SELECT_DESCRIPTIONS + ) + + +class IronOSSelectEntity(IronOSBaseEntity, SelectEntity): + """Implementation of a IronOS select entity.""" + + entity_description: IronOSSelectEntityDescription + + def __init__( + self, + coordinator: IronOSCoordinators, + entity_description: IronOSSelectEntityDescription, + ) -> None: + """Initialize the select entity.""" + super().__init__( + coordinator.live_data, entity_description, entity_description.characteristic + ) + + self.settings = coordinator.settings + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + + return self.entity_description.value_fn(self.settings.data) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + if raw_value_fn := self.entity_description.raw_value_fn: + value = raw_value_fn(option) + try: + await self.coordinator.device.write( + self.entity_description.characteristic, value + ) + except CommunicationError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="submit_setting_failed", + ) from e + await self.settings.async_request_refresh() + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + + await super().async_added_to_hass() + self.async_on_remove( + self.settings.async_add_listener( + self._handle_coordinator_update, self.entity_description.characteristic + ) + ) + await self.settings.async_request_refresh() diff --git a/homeassistant/components/iron_os/strings.json b/homeassistant/components/iron_os/strings.json index 13528104f8c..04c55280550 100644 --- a/homeassistant/components/iron_os/strings.json +++ b/homeassistant/components/iron_os/strings.json @@ -1,4 +1,8 @@ { + "common": { + "slow": "Slow", + "fast": "Fast" + }, "config": { "step": { "user": { @@ -84,6 +88,78 @@ "name": "Long-press temperature step" } }, + "select": { + "min_dc_voltage_cells": { + "name": "Power source", + "state": { + "no_battery": "External power supply (DC)", + "battery_3s": "3S (3 cells)", + "battery_4s": "4S (4 cells)", + "battery_5s": "5S (5 cells)", + "battery_6s": "6S (6 cells)" + } + }, + "orientation_mode": { + "name": "Display orientation mode", + "state": { + "right_handed": "Right-handed", + "left_handed": "Left-handed", + "auto": "Auto" + } + }, + "animation_speed": { + "name": "Animation speed", + "state": { + "off": "[%key:common::state::off%]", + "slow": "[%key:component::iron_os::common::slow%]", + "medium": "Medium", + "fast": "[%key:component::iron_os::common::fast%]" + } + }, + "autostart_mode": { + "name": "Start-up behavior", + "state": { + "disabled": "[%key:common::state::disabled%]", + "soldering": "Soldering mode", + "sleeping": "Sleeping mode", + "idle": "Idle mode" + } + }, + "temp_unit": { + "name": "Temperature display unit", + "state": { + "celsius": "Celsius (C°)", + "fahrenheit": "Fahrenheit (F°)" + } + }, + "desc_scroll_speed": { + "name": "Scrolling speed", + "state": { + "slow": "[%key:component::iron_os::common::slow%]", + "fast": "[%key:component::iron_os::common::fast%]" + } + }, + "locking_mode": { + "name": "Button locking mode", + "state": { + "off": "[%key:common::state::off%]", + "boost_only": "Boost only", + "full_locking": "Full locking" + } + }, + "logo_duration": { + "name": "Boot logo duration", + "state": { + "off": "[%key:common::state::off%]", + "seconds_1": "1 second", + "seconds_2": "2 second", + "seconds_3": "3 second", + "seconds_4": "4 second", + "seconds_5": "5 second", + "loop": "Loop" + } + } + }, "sensor": { "live_temperature": { "name": "Tip temperature" diff --git a/tests/components/iron_os/conftest.py b/tests/components/iron_os/conftest.py index 9091694e6a5..356c7358c55 100644 --- a/tests/components/iron_os/conftest.py +++ b/tests/components/iron_os/conftest.py @@ -6,12 +6,20 @@ from unittest.mock import AsyncMock, MagicMock, patch from bleak.backends.device import BLEDevice from habluetooth import BluetoothServiceInfoBleak from pynecil import ( + AnimationSpeed, + AutostartMode, + BatteryType, DeviceInfoResponse, LatestRelease, LiveDataResponse, + LockingMode, + LogoDuration, OperatingMode, PowerSource, + ScreenOrientationMode, + ScrollSpeed, SettingsDataResponse, + TempUnit, ) import pytest @@ -151,7 +159,7 @@ def mock_pynecil() -> Generator[AsyncMock]: client.get_settings.return_value = SettingsDataResponse( sleep_temp=150, sleep_timeout=5, - min_dc_voltage_cells=0, + min_dc_voltage_cells=BatteryType.BATTERY_3S, min_volltage_per_cell=3.3, qc_ideal_voltage=9.0, accel_sensitivity=7, @@ -168,6 +176,13 @@ def mock_pynecil() -> Generator[AsyncMock]: hall_sensitivity=7, pd_negotiation_timeout=2.0, display_brightness=3, + orientation_mode=ScreenOrientationMode.RIGHT_HANDED, + animation_speed=AnimationSpeed.MEDIUM, + autostart_mode=AutostartMode.IDLE, + temp_unit=TempUnit.CELSIUS, + desc_scroll_speed=ScrollSpeed.FAST, + logo_duration=LogoDuration.LOOP, + locking_mode=LockingMode.FULL_LOCKING, ) client.get_live_data.return_value = LiveDataResponse( live_temp=298, diff --git a/tests/components/iron_os/snapshots/test_select.ambr b/tests/components/iron_os/snapshots/test_select.ambr new file mode 100644 index 00000000000..ce6045c1243 --- /dev/null +++ b/tests/components/iron_os/snapshots/test_select.ambr @@ -0,0 +1,469 @@ +# serializer version: 1 +# name: test_state[select.pinecil_animation_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'slow', + 'medium', + 'fast', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_animation_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Animation speed', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_animation_speed', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_animation_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Animation speed', + 'options': list([ + 'off', + 'slow', + 'medium', + 'fast', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_animation_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'medium', + }) +# --- +# name: test_state[select.pinecil_boot_logo_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'seconds_1', + 'seconds_2', + 'seconds_3', + 'seconds_4', + 'seconds_5', + 'loop', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_boot_logo_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Boot logo duration', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_logo_duration', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_boot_logo_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Boot logo duration', + 'options': list([ + 'off', + 'seconds_1', + 'seconds_2', + 'seconds_3', + 'seconds_4', + 'seconds_5', + 'loop', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_boot_logo_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'loop', + }) +# --- +# name: test_state[select.pinecil_button_locking_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'boost_only', + 'full_locking', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_button_locking_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Button locking mode', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_locking_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_button_locking_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Button locking mode', + 'options': list([ + 'off', + 'boost_only', + 'full_locking', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_button_locking_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'full_locking', + }) +# --- +# name: test_state[select.pinecil_display_orientation_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'right_handed', + 'left_handed', + 'auto', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_display_orientation_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Display orientation mode', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_orientation_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_display_orientation_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Display orientation mode', + 'options': list([ + 'right_handed', + 'left_handed', + 'auto', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_display_orientation_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'right_handed', + }) +# --- +# name: test_state[select.pinecil_power_source-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_battery', + 'battery_3s', + 'battery_4s', + 'battery_5s', + 'battery_6s', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_power_source', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power source', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_min_dc_voltage_cells', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_power_source-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Power source', + 'options': list([ + 'no_battery', + 'battery_3s', + 'battery_4s', + 'battery_5s', + 'battery_6s', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_power_source', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'battery_3s', + }) +# --- +# name: test_state[select.pinecil_scrolling_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'slow', + 'fast', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_scrolling_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Scrolling speed', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_desc_scroll_speed', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_scrolling_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Scrolling speed', + 'options': list([ + 'slow', + 'fast', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_scrolling_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'fast', + }) +# --- +# name: test_state[select.pinecil_start_up_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'disabled', + 'soldering', + 'sleeping', + 'idle', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_start_up_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start-up behavior', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_autostart_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_start_up_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Start-up behavior', + 'options': list([ + 'disabled', + 'soldering', + 'sleeping', + 'idle', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_start_up_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_state[select.pinecil_temperature_display_unit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'celsius', + 'fahrenheit', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_temperature_display_unit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature display unit', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_temp_unit', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_temperature_display_unit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Temperature display unit', + 'options': list([ + 'celsius', + 'fahrenheit', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_temperature_display_unit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'celsius', + }) +# --- diff --git a/tests/components/iron_os/test_select.py b/tests/components/iron_os/test_select.py new file mode 100644 index 00000000000..5e981e1618e --- /dev/null +++ b/tests/components/iron_os/test_select.py @@ -0,0 +1,164 @@ +"""Tests for the IronOS select platform.""" + +from collections.abc import AsyncGenerator +from datetime import timedelta +from enum import Enum +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from pynecil import ( + AnimationSpeed, + BatteryType, + CharSetting, + CommunicationError, + LockingMode, + LogoDuration, + ScreenOrientationMode, + ScrollSpeed, + TempUnit, +) +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.fixture(autouse=True) +async def select_only() -> AsyncGenerator[None]: + """Enable only the select platform.""" + with patch( + "homeassistant.components.iron_os.PLATFORMS", + [Platform.SELECT], + ): + yield + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_pynecil", "ble_device" +) +async def test_state( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the IronOS select platform states.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + freezer.tick(timedelta(seconds=60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_id", "option", "call_params"), + [ + ( + "select.pinecil_power_source", + "battery_3s", + (CharSetting.MIN_DC_VOLTAGE_CELLS, BatteryType.BATTERY_3S), + ), + ( + "select.pinecil_display_orientation_mode", + "right_handed", + (CharSetting.ORIENTATION_MODE, ScreenOrientationMode.RIGHT_HANDED), + ), + ( + "select.pinecil_animation_speed", + "medium", + (CharSetting.ANIMATION_SPEED, AnimationSpeed.MEDIUM), + ), + ( + "select.pinecil_temperature_display_unit", + "fahrenheit", + (CharSetting.TEMP_UNIT, TempUnit.FAHRENHEIT), + ), + ( + "select.pinecil_scrolling_speed", + "fast", + (CharSetting.DESC_SCROLL_SPEED, ScrollSpeed.FAST), + ), + ( + "select.pinecil_button_locking_mode", + "full_locking", + (CharSetting.LOCKING_MODE, LockingMode.FULL_LOCKING), + ), + ( + "select.pinecil_boot_logo_duration", + "loop", + (CharSetting.LOGO_DURATION, LogoDuration.LOOP), + ), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_select_option( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, + entity_id: str, + option: str, + call_params: tuple[Enum, ...], +) -> None: + """Test the IronOS select option service.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + service_data={ATTR_OPTION: option}, + target={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert len(mock_pynecil.write.mock_calls) == 1 + mock_pynecil.write.assert_called_once_with(*call_params) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_select_option_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test the IronOS select option service exception.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + mock_pynecil.write.side_effect = CommunicationError + + with pytest.raises( + ServiceValidationError, + match="Failed to submit setting to device, try again later", + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + service_data={ATTR_OPTION: "battery_3s"}, + target={ATTR_ENTITY_ID: "select.pinecil_power_source"}, + blocking=True, + )