From adb1fbbbc453bf6c0e73fe429396970274ccdcf6 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sat, 28 Dec 2024 21:59:06 +0100 Subject: [PATCH] Add switch platform to IronOS integration (#133691) * Add switch platform * Add tests * prevent switch bouncing * some changes * icons * update tests * changes --- homeassistant/components/iron_os/__init__.py | 1 + .../components/iron_os/coordinator.py | 21 + homeassistant/components/iron_os/icons.json | 38 ++ .../components/iron_os/quality_scale.yaml | 4 +- homeassistant/components/iron_os/strings.json | 26 ++ homeassistant/components/iron_os/switch.py | 163 ++++++++ tests/components/iron_os/conftest.py | 8 + .../iron_os/snapshots/test_switch.ambr | 369 ++++++++++++++++++ tests/components/iron_os/test_switch.py | 146 +++++++ 9 files changed, 773 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/iron_os/switch.py create mode 100644 tests/components/iron_os/snapshots/test_switch.ambr create mode 100644 tests/components/iron_os/test_switch.py diff --git a/homeassistant/components/iron_os/__init__.py b/homeassistant/components/iron_os/__init__.py index 9655f7bfcdd..c3924c49c9a 100644 --- a/homeassistant/components/iron_os/__init__.py +++ b/homeassistant/components/iron_os/__init__.py @@ -30,6 +30,7 @@ PLATFORMS: list[Platform] = [ Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, Platform.UPDATE, ] diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py index e8ddef43bd7..339cbdcca28 100644 --- a/homeassistant/components/iron_os/coordinator.py +++ b/homeassistant/components/iron_os/coordinator.py @@ -5,8 +5,10 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging +from typing import cast from pynecil import ( + CharSetting, CommunicationError, DeviceInfoResponse, IronOSUpdate, @@ -19,6 +21,7 @@ from pynecil import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -147,3 +150,21 @@ class IronOSSettingsCoordinator(IronOSBaseCoordinator[SettingsDataResponse]): _LOGGER.debug("Failed to fetch settings", exc_info=e) return self.data or SettingsDataResponse() + + async def write(self, characteristic: CharSetting, value: bool) -> None: + """Write value to the settings characteristic.""" + + try: + await self.device.write(characteristic, value) + except CommunicationError as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="submit_setting_failed", + ) from e + + # prevent switch bouncing while waiting for coordinator to finish refresh + self.data.update( + cast(SettingsDataResponse, {characteristic.name.lower(): value}) + ) + self.async_update_listeners() + await self.async_request_refresh() diff --git a/homeassistant/components/iron_os/icons.json b/homeassistant/components/iron_os/icons.json index 0d26b027c3f..9636ef682cb 100644 --- a/homeassistant/components/iron_os/icons.json +++ b/homeassistant/components/iron_os/icons.json @@ -149,6 +149,44 @@ "estimated_power": { "default": "mdi:flash" } + }, + "switch": { + "animation_loop": { + "default": "mdi:play-box", + "state": { + "on": "mdi:animation-play" + } + }, + "calibrate_cjc": { + "default": "mdi:tune-vertical" + }, + "cooling_temp_blink": { + "default": "mdi:alarm-light-outline", + "state": { + "off": "mdi:alarm-light-off-outline" + } + }, + "display_invert": { + "default": "mdi:invert-colors" + }, + "invert_buttons": { + "default": "mdi:plus-minus-variant" + }, + "usb_pd_mode": { + "default": "mdi:meter-electric-outline" + }, + "idle_screen_details": { + "default": "mdi:card-bulleted-outline", + "state": { + "off": "mdi:card-bulleted-off-outline" + } + }, + "solder_screen_details": { + "default": "mdi:card-bulleted-outline", + "state": { + "off": "mdi:card-bulleted-off-outline" + } + } } } } diff --git a/homeassistant/components/iron_os/quality_scale.yaml b/homeassistant/components/iron_os/quality_scale.yaml index 922702b8260..fd89b80d782 100644 --- a/homeassistant/components/iron_os/quality_scale.yaml +++ b/homeassistant/components/iron_os/quality_scale.yaml @@ -26,9 +26,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: exempt - comment: Integration does not have actions + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt diff --git a/homeassistant/components/iron_os/strings.json b/homeassistant/components/iron_os/strings.json index 04c55280550..b7d6cc673a6 100644 --- a/homeassistant/components/iron_os/strings.json +++ b/homeassistant/components/iron_os/strings.json @@ -214,6 +214,32 @@ "estimated_power": { "name": "Estimated power" } + }, + "switch": { + "animation_loop": { + "name": "Animation loop" + }, + "cooling_temp_blink": { + "name": "Cool down screen flashing" + }, + "idle_screen_details": { + "name": "Detailed idle screen" + }, + "solder_screen_details": { + "name": "Detailed solder screen" + }, + "invert_buttons": { + "name": "Swap +/- buttons" + }, + "display_invert": { + "name": "Invert screen" + }, + "calibrate_cjc": { + "name": "Calibrate CJC" + }, + "usb_pd_mode": { + "name": "Power Delivery 3.1 EPR" + } } }, "exceptions": { diff --git a/homeassistant/components/iron_os/switch.py b/homeassistant/components/iron_os/switch.py new file mode 100644 index 00000000000..4e14b240ffb --- /dev/null +++ b/homeassistant/components/iron_os/switch.py @@ -0,0 +1,163 @@ +"""Switch platform for IronOS integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from pynecil import CharSetting, SettingsDataResponse + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import IronOSConfigEntry +from .coordinator import IronOSCoordinators +from .entity import IronOSBaseEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class IronOSSwitchEntityDescription(SwitchEntityDescription): + """Describes IronOS switch entity.""" + + is_on_fn: Callable[[SettingsDataResponse], bool | None] + characteristic: CharSetting + + +class IronOSSwitch(StrEnum): + """Switch controls for IronOS device.""" + + ANIMATION_LOOP = "animation_loop" + COOLING_TEMP_BLINK = "cooling_temp_blink" + IDLE_SCREEN_DETAILS = "idle_screen_details" + SOLDER_SCREEN_DETAILS = "solder_screen_details" + INVERT_BUTTONS = "invert_buttons" + DISPLAY_INVERT = "display_invert" + CALIBRATE_CJC = "calibrate_cjc" + USB_PD_MODE = "usb_pd_mode" + + +SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = ( + IronOSSwitchEntityDescription( + key=IronOSSwitch.ANIMATION_LOOP, + translation_key=IronOSSwitch.ANIMATION_LOOP, + characteristic=CharSetting.ANIMATION_LOOP, + is_on_fn=lambda x: x.get("animation_loop"), + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + ), + IronOSSwitchEntityDescription( + key=IronOSSwitch.COOLING_TEMP_BLINK, + translation_key=IronOSSwitch.COOLING_TEMP_BLINK, + characteristic=CharSetting.COOLING_TEMP_BLINK, + is_on_fn=lambda x: x.get("cooling_temp_blink"), + entity_category=EntityCategory.CONFIG, + ), + IronOSSwitchEntityDescription( + key=IronOSSwitch.IDLE_SCREEN_DETAILS, + translation_key=IronOSSwitch.IDLE_SCREEN_DETAILS, + characteristic=CharSetting.IDLE_SCREEN_DETAILS, + is_on_fn=lambda x: x.get("idle_screen_details"), + entity_category=EntityCategory.CONFIG, + ), + IronOSSwitchEntityDescription( + key=IronOSSwitch.SOLDER_SCREEN_DETAILS, + translation_key=IronOSSwitch.SOLDER_SCREEN_DETAILS, + characteristic=CharSetting.SOLDER_SCREEN_DETAILS, + is_on_fn=lambda x: x.get("solder_screen_details"), + entity_category=EntityCategory.CONFIG, + ), + IronOSSwitchEntityDescription( + key=IronOSSwitch.INVERT_BUTTONS, + translation_key=IronOSSwitch.INVERT_BUTTONS, + characteristic=CharSetting.INVERT_BUTTONS, + is_on_fn=lambda x: x.get("invert_buttons"), + entity_category=EntityCategory.CONFIG, + ), + IronOSSwitchEntityDescription( + key=IronOSSwitch.DISPLAY_INVERT, + translation_key=IronOSSwitch.DISPLAY_INVERT, + characteristic=CharSetting.DISPLAY_INVERT, + is_on_fn=lambda x: x.get("display_invert"), + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + ), + IronOSSwitchEntityDescription( + key=IronOSSwitch.CALIBRATE_CJC, + translation_key=IronOSSwitch.CALIBRATE_CJC, + characteristic=CharSetting.CALIBRATE_CJC, + is_on_fn=lambda x: x.get("calibrate_cjc"), + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + ), + IronOSSwitchEntityDescription( + key=IronOSSwitch.USB_PD_MODE, + translation_key=IronOSSwitch.USB_PD_MODE, + characteristic=CharSetting.USB_PD_MODE, + is_on_fn=lambda x: x.get("usb_pd_mode"), + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: IronOSConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches from a config entry.""" + + coordinators = entry.runtime_data + + async_add_entities( + IronOSSwitchEntity(coordinators, description) + for description in SWITCH_DESCRIPTIONS + ) + + +class IronOSSwitchEntity(IronOSBaseEntity, SwitchEntity): + """Representation of a IronOS Switch.""" + + entity_description: IronOSSwitchEntityDescription + + def __init__( + self, + coordinators: IronOSCoordinators, + entity_description: IronOSSwitchEntityDescription, + ) -> None: + """Initialize the switch entity.""" + super().__init__(coordinators.live_data, entity_description) + + self.settings = coordinators.settings + + @property + def is_on(self) -> bool | None: + """Return the state of the device.""" + return self.entity_description.is_on_fn( + self.settings.data, + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.settings.write(self.entity_description.characteristic, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.settings.write(self.entity_description.characteristic, False) + + 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/tests/components/iron_os/conftest.py b/tests/components/iron_os/conftest.py index 356c7358c55..f14043c096e 100644 --- a/tests/components/iron_os/conftest.py +++ b/tests/components/iron_os/conftest.py @@ -183,6 +183,14 @@ def mock_pynecil() -> Generator[AsyncMock]: desc_scroll_speed=ScrollSpeed.FAST, logo_duration=LogoDuration.LOOP, locking_mode=LockingMode.FULL_LOCKING, + animation_loop=True, + cooling_temp_blink=True, + idle_screen_details=True, + solder_screen_details=True, + invert_buttons=True, + display_invert=True, + calibrate_cjc=True, + usb_pd_mode=True, ) client.get_live_data.return_value = LiveDataResponse( live_temp=298, diff --git a/tests/components/iron_os/snapshots/test_switch.ambr b/tests/components/iron_os/snapshots/test_switch.ambr new file mode 100644 index 00000000000..0c4f5071a48 --- /dev/null +++ b/tests/components/iron_os/snapshots/test_switch.ambr @@ -0,0 +1,369 @@ +# serializer version: 1 +# name: test_switch_platform[switch.pinecil_animation_loop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_animation_loop', + '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 loop', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_animation_loop', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_animation_loop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Animation loop', + }), + 'context': , + 'entity_id': 'switch.pinecil_animation_loop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_calibrate_cjc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_calibrate_cjc', + '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': 'Calibrate CJC', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_calibrate_cjc', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_calibrate_cjc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Calibrate CJC', + }), + 'context': , + 'entity_id': 'switch.pinecil_calibrate_cjc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_cool_down_screen_flashing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_cool_down_screen_flashing', + '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': 'Cool down screen flashing', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_cooling_temp_blink', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_cool_down_screen_flashing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Cool down screen flashing', + }), + 'context': , + 'entity_id': 'switch.pinecil_cool_down_screen_flashing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_detailed_idle_screen-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_detailed_idle_screen', + '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': 'Detailed idle screen', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_idle_screen_details', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_detailed_idle_screen-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Detailed idle screen', + }), + 'context': , + 'entity_id': 'switch.pinecil_detailed_idle_screen', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_detailed_solder_screen-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_detailed_solder_screen', + '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': 'Detailed solder screen', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_solder_screen_details', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_detailed_solder_screen-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Detailed solder screen', + }), + 'context': , + 'entity_id': 'switch.pinecil_detailed_solder_screen', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_invert_screen-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_invert_screen', + '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': 'Invert screen', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_display_invert', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_invert_screen-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Invert screen', + }), + 'context': , + 'entity_id': 'switch.pinecil_invert_screen', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_power_delivery_3_1_epr-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_power_delivery_3_1_epr', + '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 Delivery 3.1 EPR', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_usb_pd_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_power_delivery_3_1_epr-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Power Delivery 3.1 EPR', + }), + 'context': , + 'entity_id': 'switch.pinecil_power_delivery_3_1_epr', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_swap_buttons-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_swap_buttons', + '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': 'Swap +/- buttons', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_invert_buttons', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_swap_buttons-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Swap +/- buttons', + }), + 'context': , + 'entity_id': 'switch.pinecil_swap_buttons', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/iron_os/test_switch.py b/tests/components/iron_os/test_switch.py new file mode 100644 index 00000000000..4f964133d0a --- /dev/null +++ b/tests/components/iron_os/test_switch.py @@ -0,0 +1,146 @@ +"""Tests for the IronOS switch platform.""" + +from collections.abc import AsyncGenerator +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from pynecil import CharSetting, CommunicationError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +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 switch_only() -> AsyncGenerator[None]: + """Enable only the switch platform.""" + with patch( + "homeassistant.components.iron_os.PLATFORMS", + [Platform.SWITCH], + ): + yield + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_pynecil", "ble_device" +) +async def test_switch_platform( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the IronOS switch platform.""" + 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=3)) + 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", "target"), + [ + ("switch.pinecil_animation_loop", CharSetting.ANIMATION_LOOP), + ("switch.pinecil_calibrate_cjc", CharSetting.CALIBRATE_CJC), + ("switch.pinecil_cool_down_screen_flashing", CharSetting.COOLING_TEMP_BLINK), + ("switch.pinecil_detailed_idle_screen", CharSetting.IDLE_SCREEN_DETAILS), + ("switch.pinecil_detailed_solder_screen", CharSetting.SOLDER_SCREEN_DETAILS), + ("switch.pinecil_invert_screen", CharSetting.DISPLAY_INVERT), + ("switch.pinecil_power_delivery_3_1_epr", CharSetting.USB_PD_MODE), + ("switch.pinecil_swap_buttons", CharSetting.INVERT_BUTTONS), + ], +) +@pytest.mark.parametrize( + ("service", "value"), + [ + (SERVICE_TOGGLE, False), + (SERVICE_TURN_OFF, False), + (SERVICE_TURN_ON, True), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_turn_on_off_toggle( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, + freezer: FrozenDateTimeFactory, + service: str, + value: bool, + entity_id: str, + target: CharSetting, +) -> None: + """Test the IronOS switch turn on/off, toggle services.""" + + 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=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + service, + service_data={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert len(mock_pynecil.write.mock_calls) == 1 + mock_pynecil.write.assert_called_once_with(target, value) + + +@pytest.mark.parametrize( + "service", + [SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON], +) +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "ble_device", "mock_pynecil" +) +async def test_turn_on_off_toggle_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, + service: str, +) -> None: + """Test the IronOS switch turn on/off, toggle service exceptions.""" + + 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( + SWITCH_DOMAIN, + service, + service_data={ATTR_ENTITY_ID: "switch.pinecil_animation_loop"}, + blocking=True, + )