From 038ca082c35934e8239880b4a3dfa20cbffb286f Mon Sep 17 00:00:00 2001 From: "Dr.Blank" Date: Thu, 3 Sep 2026 16:51:40 +0530 Subject: [PATCH] Add button platform to Persang Infrared (#181161) --- .../components/persang_infrared/__init__.py | 2 +- .../components/persang_infrared/button.py | 93 +++ .../components/persang_infrared/entity.py | 13 +- .../components/persang_infrared/icons.json | 48 ++ .../persang_infrared/quality_scale.yaml | 5 +- .../components/persang_infrared/strings.json | 46 ++ .../snapshots/test_button.ambr | 701 ++++++++++++++++++ .../persang_infrared/test_button.py | 92 +++ 8 files changed, 993 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/persang_infrared/button.py create mode 100644 homeassistant/components/persang_infrared/icons.json create mode 100644 tests/components/persang_infrared/snapshots/test_button.ambr create mode 100644 tests/components/persang_infrared/test_button.py diff --git a/homeassistant/components/persang_infrared/__init__.py b/homeassistant/components/persang_infrared/__init__.py index 5f9f3eeb3b20..a2d2d16dfd2a 100644 --- a/homeassistant/components/persang_infrared/__init__.py +++ b/homeassistant/components/persang_infrared/__init__.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -PLATFORMS = [Platform.MEDIA_PLAYER] +PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/persang_infrared/button.py b/homeassistant/components/persang_infrared/button.py new file mode 100644 index 000000000000..ebe7a744f1c3 --- /dev/null +++ b/homeassistant/components/persang_infrared/button.py @@ -0,0 +1,93 @@ +"""Button platform for the Persang Infrared integration. + +Only commands the media player has no slot for live here: the mode and EQ +cycles, scan and repeat, and the numeric keypad. +""" + +from dataclasses import dataclass +from typing import override + +from infrared_protocols.codes.persang.speaker import PersangSpeakerCode + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import CONF_INFRARED_EMITTER_ENTITY_ID +from .entity import PersangIrEntity + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class PersangIrButtonEntityDescription(ButtonEntityDescription): + """Describes a Persang IR button entity.""" + + command_code: PersangSpeakerCode + + +BUTTON_DESCRIPTIONS: tuple[PersangIrButtonEntityDescription, ...] = ( + PersangIrButtonEntityDescription( + key="mode", + translation_key="mode", + command_code=PersangSpeakerCode.MODE, + ), + PersangIrButtonEntityDescription( + key="eq", + translation_key="eq", + command_code=PersangSpeakerCode.EQ, + ), + PersangIrButtonEntityDescription( + key="scan", + translation_key="scan", + command_code=PersangSpeakerCode.SCAN, + ), + PersangIrButtonEntityDescription( + key="repeat", + translation_key="repeat", + command_code=PersangSpeakerCode.REPEAT, + ), + *( + PersangIrButtonEntityDescription( + key=f"num_{digit}", + translation_key=f"num_{digit}", + command_code=PersangSpeakerCode[f"NUM_{digit}"], + ) + for digit in range(10) + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Persang IR buttons from a config entry.""" + infrared_entity_id = entry.data[CONF_INFRARED_EMITTER_ENTITY_ID] + async_add_entities( + PersangIrButton(entry, infrared_entity_id, description) + for description in BUTTON_DESCRIPTIONS + ) + + +class PersangIrButton(PersangIrEntity, ButtonEntity): + """Persang IR button entity.""" + + entity_description: PersangIrButtonEntityDescription + + def __init__( + self, + entry: ConfigEntry, + infrared_entity_id: str, + description: PersangIrButtonEntityDescription, + ) -> None: + """Initialize Persang IR button.""" + super().__init__(entry, infrared_entity_id, unique_id_suffix=description.key) + self.entity_description = description + + @override + async def async_press(self) -> None: + """Press the button.""" + await self._send_command(self.entity_description.command_code.to_command()) diff --git a/homeassistant/components/persang_infrared/entity.py b/homeassistant/components/persang_infrared/entity.py index f90f3f88d46e..22777214e776 100644 --- a/homeassistant/components/persang_infrared/entity.py +++ b/homeassistant/components/persang_infrared/entity.py @@ -12,10 +12,19 @@ class PersangIrEntity(InfraredEmitterConsumerEntity): _attr_has_entity_name = True - def __init__(self, entry: ConfigEntry, infrared_entity_id: str) -> None: + def __init__( + self, + entry: ConfigEntry, + infrared_entity_id: str, + unique_id_suffix: str | None = None, + ) -> None: """Initialize Persang IR entity.""" self._infrared_emitter_entity_id = infrared_entity_id - self._attr_unique_id = entry.entry_id + self._attr_unique_id = ( + entry.entry_id + if unique_id_suffix is None + else f"{entry.entry_id}_{unique_id_suffix}" + ) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.entry_id)}, name="Persang speaker", diff --git a/homeassistant/components/persang_infrared/icons.json b/homeassistant/components/persang_infrared/icons.json new file mode 100644 index 000000000000..46fa6f90d5b3 --- /dev/null +++ b/homeassistant/components/persang_infrared/icons.json @@ -0,0 +1,48 @@ +{ + "entity": { + "button": { + "eq": { + "default": "mdi:equalizer" + }, + "mode": { + "default": "mdi:playlist-music" + }, + "num_0": { + "default": "mdi:numeric-0" + }, + "num_1": { + "default": "mdi:numeric-1" + }, + "num_2": { + "default": "mdi:numeric-2" + }, + "num_3": { + "default": "mdi:numeric-3" + }, + "num_4": { + "default": "mdi:numeric-4" + }, + "num_5": { + "default": "mdi:numeric-5" + }, + "num_6": { + "default": "mdi:numeric-6" + }, + "num_7": { + "default": "mdi:numeric-7" + }, + "num_8": { + "default": "mdi:numeric-8" + }, + "num_9": { + "default": "mdi:numeric-9" + }, + "repeat": { + "default": "mdi:repeat" + }, + "scan": { + "default": "mdi:motion-play" + } + } + } +} diff --git a/homeassistant/components/persang_infrared/quality_scale.yaml b/homeassistant/components/persang_infrared/quality_scale.yaml index f4e31311defa..2de29a842a6c 100644 --- a/homeassistant/components/persang_infrared/quality_scale.yaml +++ b/homeassistant/components/persang_infrared/quality_scale.yaml @@ -100,10 +100,7 @@ rules: status: exempt comment: | This integration does not raise exceptions. - icon-translations: - status: exempt - comment: | - This integration does not use custom icons. + icon-translations: done reconfiguration-flow: todo repair-issues: status: exempt diff --git a/homeassistant/components/persang_infrared/strings.json b/homeassistant/components/persang_infrared/strings.json index a74f5916bfb4..3409e3fc2fcd 100644 --- a/homeassistant/components/persang_infrared/strings.json +++ b/homeassistant/components/persang_infrared/strings.json @@ -16,5 +16,51 @@ "title": "Set up Persang IR speaker" } } + }, + "entity": { + "button": { + "eq": { + "name": "Equalizer" + }, + "mode": { + "name": "Mode" + }, + "num_0": { + "name": "[%key:common::entity::button::num_0::name%]" + }, + "num_1": { + "name": "[%key:common::entity::button::num_1::name%]" + }, + "num_2": { + "name": "[%key:common::entity::button::num_2::name%]" + }, + "num_3": { + "name": "[%key:common::entity::button::num_3::name%]" + }, + "num_4": { + "name": "[%key:common::entity::button::num_4::name%]" + }, + "num_5": { + "name": "[%key:common::entity::button::num_5::name%]" + }, + "num_6": { + "name": "[%key:common::entity::button::num_6::name%]" + }, + "num_7": { + "name": "[%key:common::entity::button::num_7::name%]" + }, + "num_8": { + "name": "[%key:common::entity::button::num_8::name%]" + }, + "num_9": { + "name": "[%key:common::entity::button::num_9::name%]" + }, + "repeat": { + "name": "Repeat" + }, + "scan": { + "name": "Scan" + } + } } } diff --git a/tests/components/persang_infrared/snapshots/test_button.ambr b/tests/components/persang_infrared/snapshots/test_button.ambr new file mode 100644 index 000000000000..db1b9ba07e94 --- /dev/null +++ b/tests/components/persang_infrared/snapshots/test_button.ambr @@ -0,0 +1,701 @@ +# serializer version: 1 +# name: test_entities[button.persang_speaker_equalizer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_equalizer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Equalizer', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Equalizer', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'eq', + 'unique_id': '01JTEST0000000000000000000_eq', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_equalizer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Equalizer', + }), + 'context': , + 'entity_id': 'button.persang_speaker_equalizer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Mode', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mode', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mode', + 'unique_id': '01JTEST0000000000000000000_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Mode', + }), + 'context': , + 'entity_id': 'button.persang_speaker_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 0', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 0', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_0', + 'unique_id': '01JTEST0000000000000000000_num_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 0', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 1', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 1', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_1', + 'unique_id': '01JTEST0000000000000000000_num_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 1', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 2', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 2', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_2', + 'unique_id': '01JTEST0000000000000000000_num_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 2', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 3', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 3', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_3', + 'unique_id': '01JTEST0000000000000000000_num_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 3', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 4', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 4', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_4', + 'unique_id': '01JTEST0000000000000000000_num_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 4', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 5', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 5', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_5', + 'unique_id': '01JTEST0000000000000000000_num_5', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 5', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 6', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 6', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_6', + 'unique_id': '01JTEST0000000000000000000_num_6', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 6', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_7-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_7', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 7', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 7', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_7', + 'unique_id': '01JTEST0000000000000000000_num_7', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_7-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 7', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_7', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_8-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_8', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 8', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 8', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_8', + 'unique_id': '01JTEST0000000000000000000_num_8', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_8-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 8', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_8', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_number_9-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_number_9', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Number 9', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Number 9', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'num_9', + 'unique_id': '01JTEST0000000000000000000_num_9', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_number_9-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Number 9', + }), + 'context': , + 'entity_id': 'button.persang_speaker_number_9', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_repeat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_repeat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Repeat', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Repeat', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'repeat', + 'unique_id': '01JTEST0000000000000000000_repeat', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_repeat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Repeat', + }), + 'context': , + 'entity_id': 'button.persang_speaker_repeat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button.persang_speaker_scan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.persang_speaker_scan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Scan', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Scan', + 'platform': 'persang_infrared', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'scan', + 'unique_id': '01JTEST0000000000000000000_scan', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.persang_speaker_scan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Persang speaker Scan', + }), + 'context': , + 'entity_id': 'button.persang_speaker_scan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/persang_infrared/test_button.py b/tests/components/persang_infrared/test_button.py new file mode 100644 index 000000000000..8e8710ac2f1c --- /dev/null +++ b/tests/components/persang_infrared/test_button.py @@ -0,0 +1,92 @@ +"""Tests for the Persang Infrared button platform.""" + +from infrared_protocols.codes.persang.speaker import PersangSpeakerCode +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .conftest import MOCK_INFRARED_EMITTER_ENTITY_ID + +from tests.common import MockConfigEntry, snapshot_platform +from tests.components.common import assert_availability_follows_source_entity +from tests.components.infrared.common import MockInfraredEmitterEntity + +BUTTON_ENTITY_ID_MODE = "button.persang_speaker_mode" + + +@pytest.fixture +def platforms() -> list[Platform]: + """Return platforms to set up.""" + return [Platform.BUTTON] + + +@pytest.mark.usefixtures("init_integration") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test all button entities are created with the correct attributes.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + device_entry = device_registry.async_get_device_by_identifier( + ("persang_infrared", mock_config_entry.entry_id), mock_config_entry.entry_id + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("entity_id", "expected_code"), + [ + ("button.persang_speaker_mode", PersangSpeakerCode.MODE), + ("button.persang_speaker_equalizer", PersangSpeakerCode.EQ), + ("button.persang_speaker_scan", PersangSpeakerCode.SCAN), + ("button.persang_speaker_repeat", PersangSpeakerCode.REPEAT), + ("button.persang_speaker_number_0", PersangSpeakerCode.NUM_0), + ("button.persang_speaker_number_1", PersangSpeakerCode.NUM_1), + ("button.persang_speaker_number_2", PersangSpeakerCode.NUM_2), + ("button.persang_speaker_number_3", PersangSpeakerCode.NUM_3), + ("button.persang_speaker_number_4", PersangSpeakerCode.NUM_4), + ("button.persang_speaker_number_5", PersangSpeakerCode.NUM_5), + ("button.persang_speaker_number_6", PersangSpeakerCode.NUM_6), + ("button.persang_speaker_number_7", PersangSpeakerCode.NUM_7), + ("button.persang_speaker_number_8", PersangSpeakerCode.NUM_8), + ("button.persang_speaker_number_9", PersangSpeakerCode.NUM_9), + ], +) +@pytest.mark.usefixtures("init_integration") +async def test_button_press_sends_correct_code( + hass: HomeAssistant, + mock_infrared_emitter_entity: MockInfraredEmitterEntity, + entity_id: str, + expected_code: PersangSpeakerCode, +) -> None: + """Test pressing a button sends the correct IR code.""" + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(mock_infrared_emitter_entity.send_command_calls) == 1 + assert mock_infrared_emitter_entity.send_command_calls[0] == expected_code + + +@pytest.mark.usefixtures("init_integration") +async def test_button_availability_follows_ir_entity(hass: HomeAssistant) -> None: + """Test a button becomes unavailable when the IR entity is unavailable.""" + await assert_availability_follows_source_entity( + hass, BUTTON_ENTITY_ID_MODE, MOCK_INFRARED_EMITTER_ENTITY_ID + )