Skip to content

Commit

Permalink
Add button platform to IronOS integration (home-assistant#133678)
Browse files Browse the repository at this point in the history
* Add button platform to IronOS integration

* Add tests

* load platform

* refactor

* update tests
  • Loading branch information
tr4nt0r authored Dec 29, 2024
1 parent da96e20 commit 0dd93a1
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 0 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/iron_os/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Expand Down
85 changes: 85 additions & 0 deletions homeassistant/components/iron_os/button.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 8 additions & 0 deletions homeassistant/components/iron_os/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
}
}
},
"button": {
"settings_save": {
"default": "mdi:content-save-cog"
},
"settings_reset": {
"default": "mdi:refresh"
}
},
"number": {
"setpoint_temperature": {
"default": "mdi:thermometer"
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/iron_os/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
93 changes: 93 additions & 0 deletions tests/components/iron_os/snapshots/test_button.ambr
Original file line number Diff line number Diff line change
@@ -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',
})
# ---
106 changes: 106 additions & 0 deletions tests/components/iron_os/test_button.py
Original file line number Diff line number Diff line change
@@ -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,
)

0 comments on commit 0dd93a1

Please sign in to comment.