Add button platform to IronOS integration (#133678)

* Add button platform to IronOS integration

* Add tests

* load platform

* refactor

* update tests
This commit is contained in:
Manu 2024-12-29 12:39:13 +01:00 committed by GitHub
parent da96e2077b
commit 0dd93a18c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 301 additions and 0 deletions

View File

@ -27,6 +27,7 @@ from .coordinator import (
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,

View File

@ -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)

View File

@ -8,6 +8,14 @@
}
}
},
"button": {
"settings_save": {
"default": "mdi:content-save-cog"
},
"settings_reset": {
"default": "mdi:refresh"
}
},
"number": {
"setpoint_temperature": {
"default": "mdi:thermometer"

View File

@ -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"

View File

@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.pinecil_restore_default_settings',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <IronOSButton.SETTINGS_RESET: 'settings_reset'>,
'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': <ANY>,
'entity_id': 'button.pinecil_restore_default_settings',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_button_platform[button.pinecil_save_settings-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.pinecil_save_settings',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <IronOSButton.SETTINGS_SAVE: 'settings_save'>,
'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': <ANY>,
'entity_id': 'button.pinecil_save_settings',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@ -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,
)