From 0dd93a18c5383066155bdef5a203760c60762087 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sun, 29 Dec 2024 12:39:13 +0100 Subject: [PATCH] Add button platform to IronOS integration (#133678) * Add button platform to IronOS integration * Add tests * load platform * refactor * update tests --- homeassistant/components/iron_os/__init__.py | 1 + homeassistant/components/iron_os/button.py | 85 ++++++++++++++ homeassistant/components/iron_os/icons.json | 8 ++ homeassistant/components/iron_os/strings.json | 8 ++ .../iron_os/snapshots/test_button.ambr | 93 +++++++++++++++ tests/components/iron_os/test_button.py | 106 ++++++++++++++++++ 6 files changed, 301 insertions(+) create mode 100644 homeassistant/components/iron_os/button.py create mode 100644 tests/components/iron_os/snapshots/test_button.ambr create mode 100644 tests/components/iron_os/test_button.py diff --git a/homeassistant/components/iron_os/__init__.py b/homeassistant/components/iron_os/__init__.py index c3924c49c9a..6af6abb1436 100644 --- a/homeassistant/components/iron_os/__init__.py +++ b/homeassistant/components/iron_os/__init__.py @@ -27,6 +27,7 @@ from .coordinator import ( PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/iron_os/button.py b/homeassistant/components/iron_os/button.py new file mode 100644 index 00000000000..be16148a656 --- /dev/null +++ b/homeassistant/components/iron_os/button.py @@ -0,0 +1,85 @@ +"""Button platform for IronOS integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + +from pynecil import CharSetting + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +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 IronOSButtonEntityDescription(ButtonEntityDescription): + """Describes IronOS button entity.""" + + characteristic: CharSetting + + +class IronOSButton(StrEnum): + """Button controls for IronOS device.""" + + SETTINGS_RESET = "settings_reset" + SETTINGS_SAVE = "settings_save" + + +BUTTON_DESCRIPTIONS: tuple[IronOSButtonEntityDescription, ...] = ( + IronOSButtonEntityDescription( + key=IronOSButton.SETTINGS_RESET, + translation_key=IronOSButton.SETTINGS_RESET, + characteristic=CharSetting.SETTINGS_RESET, + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + ), + IronOSButtonEntityDescription( + key=IronOSButton.SETTINGS_SAVE, + translation_key=IronOSButton.SETTINGS_SAVE, + characteristic=CharSetting.SETTINGS_SAVE, + entity_category=EntityCategory.CONFIG, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: IronOSConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up button entities from a config entry.""" + coordinators = entry.runtime_data + + async_add_entities( + IronOSButtonEntity(coordinators, description) + for description in BUTTON_DESCRIPTIONS + ) + + +class IronOSButtonEntity(IronOSBaseEntity, ButtonEntity): + """Implementation of a IronOS button entity.""" + + entity_description: IronOSButtonEntityDescription + + def __init__( + self, + coordinators: IronOSCoordinators, + entity_description: IronOSButtonEntityDescription, + ) -> None: + """Initialize the select entity.""" + super().__init__(coordinators.live_data, entity_description) + + self.settings = coordinators.settings + + async def async_press(self) -> None: + """Handle the button press.""" + + await self.settings.write(self.entity_description.characteristic, True) diff --git a/homeassistant/components/iron_os/icons.json b/homeassistant/components/iron_os/icons.json index 9636ef682cb..b05e72565b9 100644 --- a/homeassistant/components/iron_os/icons.json +++ b/homeassistant/components/iron_os/icons.json @@ -8,6 +8,14 @@ } } }, + "button": { + "settings_save": { + "default": "mdi:content-save-cog" + }, + "settings_reset": { + "default": "mdi:refresh" + } + }, "number": { "setpoint_temperature": { "default": "mdi:thermometer" diff --git a/homeassistant/components/iron_os/strings.json b/homeassistant/components/iron_os/strings.json index b7d6cc673a6..e9912add826 100644 --- a/homeassistant/components/iron_os/strings.json +++ b/homeassistant/components/iron_os/strings.json @@ -29,6 +29,14 @@ "name": "Soldering tip" } }, + "button": { + "settings_save": { + "name": "Save settings" + }, + "settings_reset": { + "name": "Restore default settings" + } + }, "number": { "setpoint_temperature": { "name": "Setpoint temperature" diff --git a/tests/components/iron_os/snapshots/test_button.ambr b/tests/components/iron_os/snapshots/test_button.ambr new file mode 100644 index 00000000000..64a71f5e424 --- /dev/null +++ b/tests/components/iron_os/snapshots/test_button.ambr @@ -0,0 +1,93 @@ +# serializer version: 1 +# name: test_button_platform[button.pinecil_restore_default_settings-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pinecil_restore_default_settings', + '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': 'Restore default settings', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_settings_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_platform[button.pinecil_restore_default_settings-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Restore default settings', + }), + 'context': , + 'entity_id': 'button.pinecil_restore_default_settings', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button_platform[button.pinecil_save_settings-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pinecil_save_settings', + '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': 'Save settings', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_settings_save', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_platform[button.pinecil_save_settings-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Save settings', + }), + 'context': , + 'entity_id': 'button.pinecil_save_settings', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/iron_os/test_button.py b/tests/components/iron_os/test_button.py new file mode 100644 index 00000000000..ce8a3ca491e --- /dev/null +++ b/tests/components/iron_os/test_button.py @@ -0,0 +1,106 @@ +"""Tests for the IronOS button platform.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch + +from pynecil import CharSetting, CommunicationError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +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, snapshot_platform + + +@pytest.fixture(autouse=True) +async def button_only() -> AsyncGenerator[None]: + """Enable only the button platform.""" + with patch( + "homeassistant.components.iron_os.PLATFORMS", + [Platform.BUTTON], + ): + yield + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_pynecil", "ble_device" +) +async def test_button_platform( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test the IronOS button 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 + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_id", "call_args"), + [ + ("button.pinecil_save_settings", (CharSetting.SETTINGS_SAVE, True)), + ("button.pinecil_restore_default_settings", (CharSetting.SETTINGS_RESET, True)), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_button_press( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, + entity_id: str, + call_args: tuple[tuple[CharSetting, bool]], +) -> None: + """Test button press method.""" + + 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( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_pynecil.write.assert_called_once_with(*call_args) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_button_press_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test button press method.""" + + 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( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.pinecil_save_settings"}, + blocking=True, + )