Add switch platform to IronOS integration (#133691)

* Add switch platform

* Add tests

* prevent switch bouncing

* some changes

* icons

* update tests

* changes
pull/134173/head
Manu 2024-12-28 21:59:06 +01:00 committed by GitHub
parent 645f2e44b9
commit adb1fbbbc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 773 additions and 3 deletions

View File

@ -30,6 +30,7 @@ PLATFORMS: list[Platform] = [
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.pinecil_animation_loop',
'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': 'Animation loop',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <IronOSSwitch.ANIMATION_LOOP: 'animation_loop'>,
'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': <ANY>,
'entity_id': 'switch.pinecil_animation_loop',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_platform[switch.pinecil_calibrate_cjc-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.pinecil_calibrate_cjc',
'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': 'Calibrate CJC',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <IronOSSwitch.CALIBRATE_CJC: 'calibrate_cjc'>,
'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': <ANY>,
'entity_id': 'switch.pinecil_calibrate_cjc',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_platform[switch.pinecil_cool_down_screen_flashing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.pinecil_cool_down_screen_flashing',
'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': 'Cool down screen flashing',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <IronOSSwitch.COOLING_TEMP_BLINK: 'cooling_temp_blink'>,
'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': <ANY>,
'entity_id': 'switch.pinecil_cool_down_screen_flashing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_platform[switch.pinecil_detailed_idle_screen-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.pinecil_detailed_idle_screen',
'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': 'Detailed idle screen',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <IronOSSwitch.IDLE_SCREEN_DETAILS: 'idle_screen_details'>,
'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': <ANY>,
'entity_id': 'switch.pinecil_detailed_idle_screen',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_platform[switch.pinecil_detailed_solder_screen-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.pinecil_detailed_solder_screen',
'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': 'Detailed solder screen',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <IronOSSwitch.SOLDER_SCREEN_DETAILS: 'solder_screen_details'>,
'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': <ANY>,
'entity_id': 'switch.pinecil_detailed_solder_screen',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_platform[switch.pinecil_invert_screen-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.pinecil_invert_screen',
'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': 'Invert screen',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <IronOSSwitch.DISPLAY_INVERT: 'display_invert'>,
'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': <ANY>,
'entity_id': 'switch.pinecil_invert_screen',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.pinecil_power_delivery_3_1_epr',
'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': 'Power Delivery 3.1 EPR',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <IronOSSwitch.USB_PD_MODE: 'usb_pd_mode'>,
'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': <ANY>,
'entity_id': 'switch.pinecil_power_delivery_3_1_epr',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_platform[switch.pinecil_swap_buttons-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.pinecil_swap_buttons',
'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': 'Swap +/- buttons',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <IronOSSwitch.INVERT_BUTTONS: 'invert_buttons'>,
'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': <ANY>,
'entity_id': 'switch.pinecil_swap_buttons',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

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