Add Persang Infrared integration (#178107)

This commit is contained in:
Dr.Blank
2026-08-29 16:13:21 +02:00
committed by GitHub
parent 84c8c76523
commit b9991954a4
17 changed files with 789 additions and 0 deletions
Generated
+2
View File
@@ -1410,6 +1410,8 @@ CLAUDE.md @home-assistant/core
/tests/components/peco/ @IceBotYT
/homeassistant/components/pegel_online/ @mib1185
/tests/components/pegel_online/ @mib1185
/homeassistant/components/persang_infrared/ @Dr-Blank
/tests/components/persang_infrared/ @Dr-Blank
/homeassistant/components/persistent_notification/ @home-assistant/core
/tests/components/persistent_notification/ @home-assistant/core
/homeassistant/components/pglab/ @pglab-electronics
@@ -0,0 +1,18 @@
"""Persang Infrared integration for Home Assistant."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Persang IR from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a Persang IR config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,51 @@
"""Config flow for the Persang Infrared integration."""
from typing import Any, override
import voluptuous as vol
from homeassistant.components.infrared import (
DOMAIN as INFRARED_DOMAIN,
async_get_emitters,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig
from .const import CONF_INFRARED_EMITTER_ENTITY_ID, DOMAIN
class PersangIrConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle config flow for Persang IR."""
VERSION = 1
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
emitter_entity_ids = async_get_emitters(self.hass)
if not emitter_entity_ids:
return self.async_abort(reason="no_emitters")
if user_input is not None:
entity_id = user_input[CONF_INFRARED_EMITTER_ENTITY_ID]
await self.async_set_unique_id(entity_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Persang speaker", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_INFRARED_EMITTER_ENTITY_ID): EntitySelector(
EntitySelectorConfig(
domain=INFRARED_DOMAIN,
include_entities=emitter_entity_ids,
)
),
}
),
)
@@ -0,0 +1,4 @@
"""Constants for the Persang Infrared integration."""
DOMAIN = "persang_infrared"
CONF_INFRARED_EMITTER_ENTITY_ID = "infrared_emitter_entity_id"
@@ -0,0 +1,23 @@
"""Common entity for the Persang Infrared integration."""
from homeassistant.components.infrared import InfraredEmitterConsumerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo
from .const import DOMAIN
class PersangIrEntity(InfraredEmitterConsumerEntity):
"""Persang IR base entity."""
_attr_has_entity_name = True
def __init__(self, entry: ConfigEntry, infrared_entity_id: str) -> None:
"""Initialize Persang IR entity."""
self._infrared_emitter_entity_id = infrared_entity_id
self._attr_unique_id = entry.entry_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name="Persang speaker",
manufacturer="Persang",
)
@@ -0,0 +1,11 @@
{
"domain": "persang_infrared",
"name": "Persang Infrared",
"codeowners": ["@Dr-Blank"],
"config_flow": true,
"dependencies": ["infrared"],
"documentation": "https://www.home-assistant.io/integrations/persang_infrared",
"integration_type": "device",
"iot_class": "assumed_state",
"quality_scale": "bronze"
}
@@ -0,0 +1,121 @@
"""Media player platform for the Persang Infrared integration."""
from typing import override
from infrared_protocols.codes.persang.speaker import PersangSpeakerCode
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import CONF_INFRARED_EMITTER_ENTITY_ID
from .entity import PersangIrEntity
PARALLEL_UPDATES = 1
RESTORED_STATES = (
MediaPlayerState.ON,
MediaPlayerState.OFF,
MediaPlayerState.PLAYING,
MediaPlayerState.PAUSED,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Persang IR media player from a config entry."""
infrared_entity_id = entry.data[CONF_INFRARED_EMITTER_ENTITY_ID]
async_add_entities([PersangIrMediaPlayer(entry, infrared_entity_id)])
class PersangIrMediaPlayer(PersangIrEntity, MediaPlayerEntity, RestoreEntity):
"""Persang IR speaker media player entity."""
_attr_name = None
_attr_assumed_state = True
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
_attr_supported_features = (
MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PREVIOUS_TRACK
)
@override
async def async_added_to_hass(self) -> None:
"""Restore the last assumed state."""
await super().async_added_to_hass()
if (last_state := await self.async_get_last_state()) is not None and (
last_state.state in RESTORED_STATES
):
self._attr_state = MediaPlayerState(last_state.state)
@override
async def async_turn_on(self) -> None:
"""Send the power command."""
await self._send_command(PersangSpeakerCode.POWER.to_command())
self._attr_state = MediaPlayerState.ON
self.async_write_ha_state()
@override
async def async_turn_off(self) -> None:
"""Send the power command."""
await self._send_command(PersangSpeakerCode.POWER.to_command())
self._attr_state = MediaPlayerState.OFF
self.async_write_ha_state()
@override
async def async_volume_up(self) -> None:
"""Send the volume up command."""
await self._send_command(PersangSpeakerCode.VOLUME_UP.to_command())
@override
async def async_volume_down(self) -> None:
"""Send the volume down command."""
await self._send_command(PersangSpeakerCode.VOLUME_DOWN.to_command())
@override
async def async_mute_volume(self, mute: bool) -> None:
"""Send the mute command."""
await self._send_command(PersangSpeakerCode.MUTE.to_command())
self._attr_is_volume_muted = mute
self.async_write_ha_state()
@override
async def async_media_play(self) -> None:
"""Send the play/pause command."""
await self._send_command(PersangSpeakerCode.PLAY_PAUSE.to_command())
self._attr_state = MediaPlayerState.PLAYING
self.async_write_ha_state()
@override
async def async_media_pause(self) -> None:
"""Send the play/pause command."""
await self._send_command(PersangSpeakerCode.PLAY_PAUSE.to_command())
self._attr_state = MediaPlayerState.PAUSED
self.async_write_ha_state()
@override
async def async_media_next_track(self) -> None:
"""Send the next track command."""
await self._send_command(PersangSpeakerCode.NEXT.to_command())
@override
async def async_media_previous_track(self) -> None:
"""Send the previous track command."""
await self._send_command(PersangSpeakerCode.PREVIOUS.to_command())
@@ -0,0 +1,127 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling:
status: exempt
comment: |
This integration does not poll.
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data:
status: exempt
comment: |
This integration does not store runtime data.
test-before-configure:
status: exempt
comment: |
This integration only proxies commands through an existing infrared
entity, so there is no connection to test in the config flow.
test-before-setup:
status: exempt
comment: |
This integration only proxies commands through an existing infrared
entity, so there is no separate connection to validate during setup.
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
This integration does not register custom actions.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: |
This integration does not require authentication.
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration does not support discovery.
discovery:
status: exempt
comment: |
This integration is configured manually via config flow.
docs-data-update:
status: exempt
comment: |
This integration does not fetch data from devices.
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
Each config entry creates a single device.
entity-category:
status: exempt
comment: |
The media player entity is the primary entity and does not need a category.
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
No entities should be disabled by default.
entity-translations: done
exception-translations:
status: exempt
comment: |
This integration does not raise exceptions.
icon-translations:
status: exempt
comment: |
This integration does not use custom icons.
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration does not have repairable issues.
stale-devices:
status: exempt
comment: |
Each config entry manages exactly one device.
# Platinum
async-dependency:
status: exempt
comment: |
This integration depends on infrared_protocols, which provides only code
definitions with no I/O, so async dependency does not apply.
inject-websession:
status: exempt
comment: |
This integration does not make HTTP requests.
strict-typing: todo
@@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "This Persang speaker has already been configured with this transmitter.",
"no_emitters": "No infrared transmitter entities found. Please set up an infrared device first."
},
"step": {
"user": {
"data": {
"infrared_emitter_entity_id": "Infrared transmitter"
},
"data_description": {
"infrared_emitter_entity_id": "The infrared transmitter entity to use for sending commands."
},
"description": "Select the infrared transmitter entity to use for controlling your Persang speaker.",
"title": "Set up Persang IR speaker"
}
}
}
}
+1
View File
@@ -599,6 +599,7 @@ FLOWS = {
"peblar",
"peco",
"pegel_online",
"persang_infrared",
"pglab",
"philips_js",
"pi_hole",
@@ -5497,6 +5497,12 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
"persang_infrared": {
"name": "Persang Infrared",
"integration_type": "device",
"config_flow": true,
"iot_class": "assumed_state"
},
"pge": {
"name": "Pacific Gas & Electric (PG&E)",
"integration_type": "virtual",
@@ -0,0 +1 @@
"""Tests for the Persang Infrared integration."""
@@ -0,0 +1,72 @@
"""Common fixtures for the Persang Infrared tests."""
from collections.abc import Generator
from unittest.mock import MagicMock, patch
from infrared_protocols.codes.persang.speaker import PersangSpeakerCode
import pytest
from homeassistant.components.persang_infrared import PLATFORMS
from homeassistant.components.persang_infrared.const import (
CONF_INFRARED_EMITTER_ENTITY_ID,
DOMAIN,
)
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.components.infrared import (
EMITTER_ENTITY_ID as MOCK_INFRARED_EMITTER_ENTITY_ID,
)
from tests.components.infrared.common import MockInfraredEmitterEntity
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return a mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
entry_id="01JTEST0000000000000000000",
title="Persang speaker",
data={CONF_INFRARED_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID},
unique_id=MOCK_INFRARED_EMITTER_ENTITY_ID,
)
@pytest.fixture
def platforms() -> list[Platform]:
"""Return platforms to set up."""
return PLATFORMS
@pytest.fixture
def mock_persang_to_command() -> Generator[MagicMock]:
"""Patch ``PersangSpeakerCode.to_command`` to return the code itself.
This lets tests assert on the code enum rather than on raw NEC timings.
"""
with patch.object(
PersangSpeakerCode,
"to_command",
autospec=True,
side_effect=lambda self, **kwargs: self,
) as mock:
yield mock
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
mock_persang_to_command: MagicMock,
platforms: list[Platform],
) -> MockConfigEntry:
"""Set up the Persang Infrared integration for testing."""
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.persang_infrared.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry
@@ -0,0 +1,55 @@
# serializer version: 1
# name: test_entities[media_player.persang_speaker-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.persang_speaker',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': <MediaPlayerDeviceClass.SPEAKER: 'speaker'>,
'original_icon': None,
'original_name': None,
'platform': 'persang_infrared',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 17849>,
'translation_key': None,
'unique_id': '01JTEST0000000000000000000',
'unit_of_measurement': None,
})
# ---
# name: test_entities[media_player.persang_speaker-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ASSUMED_STATE: 'assumed_state'>: True,
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'speaker',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Persang speaker',
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <MediaPlayerEntityFeature: 17849>,
}),
'context': <ANY>,
'entity_id': 'media_player.persang_speaker',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
@@ -0,0 +1,71 @@
"""Tests for the Persang Infrared config flow."""
import pytest
from homeassistant.components.persang_infrared.const import (
CONF_INFRARED_EMITTER_ENTITY_ID,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .conftest import MOCK_INFRARED_EMITTER_ENTITY_ID
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
async def test_user_flow_success(hass: HomeAssistant) -> None:
"""Test successful user config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_INFRARED_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Persang speaker"
assert result["data"] == {
CONF_INFRARED_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID
}
assert result["result"].unique_id == MOCK_INFRARED_EMITTER_ENTITY_ID
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
async def test_user_flow_already_configured(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test user flow aborts when the transmitter is already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_INFRARED_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
@pytest.mark.usefixtures("init_infrared")
async def test_user_flow_no_emitters(hass: HomeAssistant) -> None:
"""Test user flow aborts when no infrared emitters exist."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "no_emitters"
@@ -0,0 +1,19 @@
"""Tests for the Persang Infrared integration setup."""
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_setup_and_unload_entry(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
"""Test setting up and unloading a config entry."""
entry = init_integration
assert entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
@@ -0,0 +1,187 @@
"""Tests for the Persang Infrared media player platform."""
from unittest.mock import patch
from infrared_protocols.codes.persang.speaker import PersangSpeakerCode
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_MUTED,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_UP,
MediaPlayerState,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant, State
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, mock_restore_cache, snapshot_platform
from tests.components.common import assert_availability_follows_source_entity
from tests.components.infrared.common import MockInfraredEmitterEntity
MEDIA_PLAYER_ENTITY_ID = "media_player.persang_speaker"
@pytest.fixture
def platforms() -> list[Platform]:
"""Return platforms to set up."""
return [Platform.MEDIA_PLAYER]
@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 the media player entity is 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(
("service", "expected_code", "expected_state"),
[
(SERVICE_TURN_ON, PersangSpeakerCode.POWER, MediaPlayerState.ON),
(SERVICE_TURN_OFF, PersangSpeakerCode.POWER, MediaPlayerState.OFF),
(SERVICE_MEDIA_PLAY, PersangSpeakerCode.PLAY_PAUSE, MediaPlayerState.PLAYING),
(SERVICE_MEDIA_PAUSE, PersangSpeakerCode.PLAY_PAUSE, MediaPlayerState.PAUSED),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_state_changing_commands(
hass: HomeAssistant,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
service: str,
expected_code: PersangSpeakerCode,
expected_state: MediaPlayerState,
) -> None:
"""Test commands that send a code and update the assumed state."""
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
service,
{ATTR_ENTITY_ID: MEDIA_PLAYER_ENTITY_ID},
blocking=True,
)
assert mock_infrared_emitter_entity.send_command_calls == [expected_code]
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
assert state
assert state.state == expected_state
@pytest.mark.parametrize(
("service", "expected_code"),
[
(SERVICE_VOLUME_UP, PersangSpeakerCode.VOLUME_UP),
(SERVICE_VOLUME_DOWN, PersangSpeakerCode.VOLUME_DOWN),
(SERVICE_MEDIA_NEXT_TRACK, PersangSpeakerCode.NEXT),
(SERVICE_MEDIA_PREVIOUS_TRACK, PersangSpeakerCode.PREVIOUS),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_stateless_commands(
hass: HomeAssistant,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
service: str,
expected_code: PersangSpeakerCode,
) -> None:
"""Test commands that only send a code."""
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
service,
{ATTR_ENTITY_ID: MEDIA_PLAYER_ENTITY_ID},
blocking=True,
)
assert mock_infrared_emitter_entity.send_command_calls == [expected_code]
@pytest.mark.parametrize("mute", [True, False])
@pytest.mark.usefixtures("init_integration")
async def test_mute(
hass: HomeAssistant,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
mute: bool,
) -> None:
"""Test muting sends the mute toggle and tracks the requested state."""
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_MUTE,
{ATTR_ENTITY_ID: MEDIA_PLAYER_ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: mute},
blocking=True,
)
assert mock_infrared_emitter_entity.send_command_calls == [PersangSpeakerCode.MUTE]
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
assert state
assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is mute
@pytest.mark.usefixtures("init_integration")
async def test_state_unknown_without_restored_state(hass: HomeAssistant) -> None:
"""Test the entity starts as unknown when nothing was restored."""
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
assert state
assert state.state == STATE_UNKNOWN
@pytest.mark.parametrize(
"restored_state",
[
MediaPlayerState.ON,
MediaPlayerState.OFF,
MediaPlayerState.PLAYING,
MediaPlayerState.PAUSED,
],
)
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
async def test_restore_state(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
platforms: list[Platform],
restored_state: MediaPlayerState,
) -> None:
"""Test the assumed state is restored across restarts."""
mock_restore_cache(hass, [State(MEDIA_PLAYER_ENTITY_ID, restored_state)])
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.persang_infrared.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
assert state
assert state.state == restored_state
@pytest.mark.usefixtures("init_integration")
async def test_media_player_availability_follows_ir_entity(
hass: HomeAssistant,
) -> None:
"""Test the media player becomes unavailable when the IR entity is."""
await assert_availability_follows_source_entity(
hass, MEDIA_PLAYER_ENTITY_ID, MOCK_INFRARED_EMITTER_ENTITY_ID
)