mirror of
https://github.com/home-assistant/core.git
synced 2026-05-06 08:36:42 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07f1ce7fe2 | |||
| 2b65c8c992 | |||
| 7a7b0e294c | |||
| a9bcf42388 | |||
| 309afb3efb | |||
| 7e7590c8e2 | |||
| 49ab12c950 | |||
| 5d65d3e27b | |||
| 7eeea9060d | |||
| 4086d43a1b | |||
| 62dc48ddd3 |
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||
from broadlink.exceptions import BroadlinkException
|
||||
from broadlink.remote import pulses_to_data as _bl_pulses_to_data
|
||||
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEmitterEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -43,8 +43,8 @@ async def async_setup_entry(
|
||||
async_add_entities([BroadlinkInfraredEntity(device)])
|
||||
|
||||
|
||||
class BroadlinkInfraredEntity(BroadlinkEntity, InfraredEntity):
|
||||
"""Broadlink infrared transmitter entity."""
|
||||
class BroadlinkInfraredEntity(BroadlinkEntity, InfraredEmitterEntity):
|
||||
"""Broadlink infrared emitter entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "infrared_emitter"
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
|
||||
from aioesphomeapi import EntityState, InfraredCapability, InfraredInfo
|
||||
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEmitterEntity
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .entity import (
|
||||
@@ -19,8 +19,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class EsphomeInfraredEntity(EsphomeEntity[InfraredInfo, EntityState], InfraredEntity):
|
||||
"""ESPHome infrared entity using native API."""
|
||||
class EsphomeInfraredEntity(
|
||||
EsphomeEntity[InfraredInfo, EntityState], InfraredEmitterEntity
|
||||
):
|
||||
"""ESPHome infrared emitter entity using native API."""
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
"""Provides functionality to interact with infrared devices."""
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import final
|
||||
|
||||
from infrared_protocols import Command as InfraredCommand
|
||||
from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
@@ -23,15 +28,30 @@ from .const import DOMAIN
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
"InfraredEntity",
|
||||
"InfraredEntityDescription",
|
||||
"InfraredEmitterEntity",
|
||||
"InfraredEmitterEntityDescription",
|
||||
"InfraredReceivedSignal",
|
||||
"InfraredReceiverEntity",
|
||||
"InfraredReceiverEntityDescription",
|
||||
"async_get_emitters",
|
||||
"async_get_receivers",
|
||||
"async_send_command",
|
||||
"async_subscribe_receiver",
|
||||
]
|
||||
|
||||
|
||||
class InfraredDeviceClass(StrEnum):
|
||||
"""Device class for infrared entities."""
|
||||
|
||||
RECEIVER = "receiver"
|
||||
EMITTER = "emitter"
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_COMPONENT: HassKey[EntityComponent[InfraredEntity]] = HassKey(DOMAIN)
|
||||
DATA_COMPONENT: HassKey[
|
||||
EntityComponent[InfraredEmitterEntity | InfraredReceiverEntity]
|
||||
] = HassKey(DOMAIN)
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||
@@ -40,9 +60,9 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the infrared domain."""
|
||||
component = hass.data[DATA_COMPONENT] = EntityComponent[InfraredEntity](
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
component = hass.data[DATA_COMPONENT] = EntityComponent[
|
||||
InfraredEmitterEntity | InfraredReceiverEntity
|
||||
](_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
await component.async_setup(config)
|
||||
|
||||
return True
|
||||
@@ -65,7 +85,25 @@ def async_get_emitters(hass: HomeAssistant) -> list[str]:
|
||||
if component is None:
|
||||
return []
|
||||
|
||||
return [entity.entity_id for entity in component.entities]
|
||||
return [
|
||||
entity.entity_id
|
||||
for entity in component.entities
|
||||
if isinstance(entity, InfraredEmitterEntity)
|
||||
]
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_receivers(hass: HomeAssistant) -> list[str]:
|
||||
"""Get all infrared receiver entity IDs."""
|
||||
component = hass.data.get(DATA_COMPONENT)
|
||||
if component is None:
|
||||
return []
|
||||
|
||||
return [
|
||||
entity.entity_id
|
||||
for entity in component.entities
|
||||
if isinstance(entity, InfraredReceiverEntity)
|
||||
]
|
||||
|
||||
|
||||
async def async_send_command(
|
||||
@@ -89,7 +127,7 @@ async def async_send_command(
|
||||
ent_reg = er.async_get(hass)
|
||||
entity_id = er.async_validate_entity_id(ent_reg, entity_id_or_uuid)
|
||||
entity = component.get_entity(entity_id)
|
||||
if entity is None:
|
||||
if entity is None or not isinstance(entity, InfraredEmitterEntity):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entity_not_found",
|
||||
@@ -102,14 +140,62 @@ async def async_send_command(
|
||||
await entity.async_send_command_internal(command)
|
||||
|
||||
|
||||
class InfraredEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
"""Describes infrared entities."""
|
||||
@callback
|
||||
def async_subscribe_receiver(
|
||||
hass: HomeAssistant,
|
||||
entity_id_or_uuid: str,
|
||||
signal_callback: Callable[[InfraredReceivedSignal], None],
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Subscribe to IR signals from a specific receiver entity.
|
||||
|
||||
Raises:
|
||||
HomeAssistantError: If the receiver entity is not found.
|
||||
"""
|
||||
component = hass.data.get(DATA_COMPONENT)
|
||||
if component is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="component_not_loaded",
|
||||
)
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
try:
|
||||
entity_id = er.async_validate_entity_id(ent_reg, entity_id_or_uuid)
|
||||
except vol.Invalid as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="receiver_not_found",
|
||||
translation_placeholders={"entity_id": entity_id_or_uuid},
|
||||
) from err
|
||||
|
||||
entity = component.get_entity(entity_id)
|
||||
if entity is None or not isinstance(entity, InfraredReceiverEntity):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="receiver_not_found",
|
||||
translation_placeholders={"entity_id": entity_id},
|
||||
)
|
||||
|
||||
return entity.async_subscribe_received_signal(signal_callback)
|
||||
|
||||
|
||||
class InfraredEntity(RestoreEntity):
|
||||
"""Base class for infrared transmitter entities."""
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InfraredReceivedSignal:
|
||||
"""Represents a received IR signal."""
|
||||
|
||||
entity_description: InfraredEntityDescription
|
||||
timings: list[int]
|
||||
modulation: int | None = None
|
||||
|
||||
|
||||
class InfraredEmitterEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
"""Describes infrared emitter entities."""
|
||||
|
||||
|
||||
class InfraredEmitterEntity(RestoreEntity):
|
||||
"""Base class for infrared emitter entities."""
|
||||
|
||||
entity_description: InfraredEmitterEntityDescription
|
||||
_attr_device_class: InfraredDeviceClass = InfraredDeviceClass.EMITTER
|
||||
_attr_should_poll = False
|
||||
_attr_state: None = None
|
||||
|
||||
@@ -149,3 +235,72 @@ class InfraredEntity(RestoreEntity):
|
||||
Raises:
|
||||
HomeAssistantError: If transmission fails.
|
||||
"""
|
||||
|
||||
|
||||
class InfraredReceiverEntityDescription(EntityDescription, frozen_or_thawed=True):
|
||||
"""Describes infrared receiver entities."""
|
||||
|
||||
|
||||
class InfraredReceiverEntity(RestoreEntity):
|
||||
"""Base class for infrared receiver entities."""
|
||||
|
||||
entity_description: InfraredReceiverEntityDescription
|
||||
_attr_device_class: InfraredDeviceClass = InfraredDeviceClass.RECEIVER
|
||||
_attr_should_poll = False
|
||||
_attr_state: None = None
|
||||
|
||||
__last_signal_received: str | None = None
|
||||
|
||||
@cached_property
|
||||
def __signal_callbacks(self) -> set[Callable[[InfraredReceivedSignal], None]]:
|
||||
"""Subscriber callback set, lazily initialized on first access."""
|
||||
return set()
|
||||
|
||||
@property
|
||||
@final
|
||||
def state(self) -> str | None:
|
||||
"""Return the entity state."""
|
||||
return self.__last_signal_received
|
||||
|
||||
@final
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Call when the infrared entity is added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if state is not None and state.state not in (STATE_UNAVAILABLE, None):
|
||||
self.__last_signal_received = state.state
|
||||
|
||||
@final
|
||||
def _handle_received_signal(self, signal: InfraredReceivedSignal) -> None:
|
||||
"""Handle a received IR signal.
|
||||
|
||||
Should not be overridden. To be called by platform implementations when a
|
||||
signal is received.
|
||||
"""
|
||||
self.__last_signal_received = dt_util.utcnow().isoformat(
|
||||
timespec="milliseconds"
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
for signal_callback in tuple(self.__signal_callbacks):
|
||||
try:
|
||||
signal_callback(signal)
|
||||
except Exception:
|
||||
_LOGGER.exception("Error in signal callback for %s", self.entity_id)
|
||||
|
||||
@callback
|
||||
def async_subscribe_received_signal(
|
||||
self,
|
||||
signal_callback: Callable[[InfraredReceivedSignal], None],
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Subscribe to received IR signals.
|
||||
|
||||
Returns a callable to unsubscribe.
|
||||
"""
|
||||
callbacks = self.__signal_callbacks
|
||||
callbacks.add(signal_callback)
|
||||
|
||||
@callback
|
||||
def remove_callback() -> None:
|
||||
callbacks.discard(signal_callback)
|
||||
|
||||
return remove_callback
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:led-on"
|
||||
},
|
||||
"receiver": {
|
||||
"default": "mdi:led-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "Infrared emitter"
|
||||
},
|
||||
"receiver": {
|
||||
"name": "Infrared receiver"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"component_not_loaded": {
|
||||
"message": "Infrared component not loaded"
|
||||
},
|
||||
"entity_not_found": {
|
||||
"message": "Infrared entity `{entity_id}` not found"
|
||||
},
|
||||
"receiver_not_found": {
|
||||
"message": "Infrared receiver entity `{entity_id}` not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
Platform.BUTTON,
|
||||
Platform.FAN,
|
||||
Platform.EVENT,
|
||||
Platform.IMAGE,
|
||||
Platform.INFRARED,
|
||||
Platform.LAWN_MOWER,
|
||||
|
||||
@@ -9,6 +9,7 @@ from homeassistant import data_entry_flow
|
||||
from homeassistant.components.infrared import (
|
||||
DOMAIN as INFRARED_DOMAIN,
|
||||
async_get_emitters,
|
||||
async_get_receivers,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
@@ -22,7 +23,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig
|
||||
|
||||
from .const import CONF_INFRARED_ENTITY_ID, DOMAIN
|
||||
from .const import CONF_INFRARED_ENTITY_ID, CONF_INFRARED_RECEIVER_ENTITY_ID, DOMAIN
|
||||
|
||||
CONF_BOOLEAN = "bool"
|
||||
CONF_INT = "int"
|
||||
@@ -178,25 +179,33 @@ class InfraredFanSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""User flow to add an infrared fan."""
|
||||
|
||||
entities = async_get_emitters(self.hass)
|
||||
if not entities:
|
||||
return self.async_abort(reason="no_emitters")
|
||||
|
||||
if user_input is not None:
|
||||
title = user_input.pop("name")
|
||||
return self.async_create_entry(data=user_input, title=title)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("name"): str,
|
||||
vol.Required(CONF_INFRARED_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=entities,
|
||||
)
|
||||
),
|
||||
}
|
||||
emitter_entities = async_get_emitters(self.hass)
|
||||
if not emitter_entities:
|
||||
return self.async_abort(reason="no_emitters")
|
||||
|
||||
schema_dict: dict[vol.Marker, Any] = {
|
||||
vol.Required("name"): str,
|
||||
vol.Required(CONF_INFRARED_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=emitter_entities,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
receiver_entities = async_get_receivers(self.hass)
|
||||
if receiver_entities:
|
||||
schema_dict[vol.Optional(CONF_INFRARED_RECEIVER_ENTITY_ID)] = (
|
||||
EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=receiver_entities,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema_dict))
|
||||
|
||||
@@ -6,6 +6,7 @@ from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DOMAIN = "kitchen_sink"
|
||||
CONF_INFRARED_ENTITY_ID = "infrared_entity_id"
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID = "infrared_receiver_entity_id"
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
f"{DOMAIN}.backup_agent_listeners"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Demo platform that offers a fake infrared receiver event entity."""
|
||||
|
||||
from homeassistant.components.event import EventEntity
|
||||
from homeassistant.components.infrared import (
|
||||
InfraredReceivedSignal,
|
||||
async_subscribe_receiver,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from .const import CONF_INFRARED_RECEIVER_ENTITY_ID, DOMAIN
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the demo infrared event platform."""
|
||||
for subentry_id, subentry in config_entry.subentries.items():
|
||||
if subentry.subentry_type != "infrared_fan":
|
||||
continue
|
||||
if subentry.data.get(CONF_INFRARED_RECEIVER_ENTITY_ID) is None:
|
||||
continue
|
||||
async_add_entities(
|
||||
[
|
||||
DemoInfraredEvent(
|
||||
subentry_id=subentry_id,
|
||||
device_name=subentry.title,
|
||||
infrared_receiver_entity_id=subentry.data[
|
||||
CONF_INFRARED_RECEIVER_ENTITY_ID
|
||||
],
|
||||
)
|
||||
],
|
||||
config_subentry_id=subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class DemoInfraredEvent(EventEntity):
|
||||
"""Representation of a demo infrared event entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Received IR Event"
|
||||
_attr_should_poll = False
|
||||
_attr_event_types = ["unknown"]
|
||||
|
||||
def __init__(
|
||||
self, subentry_id: str, device_name: str, infrared_receiver_entity_id: str
|
||||
) -> None:
|
||||
"""Initialize the demo infrared event entity."""
|
||||
self._receiver_entity_id = infrared_receiver_entity_id
|
||||
self._attr_unique_id = subentry_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, subentry_id)}, name=device_name
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the IR receiver when added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def _handle_signal(signal: InfraredReceivedSignal) -> None:
|
||||
"""Handle a received IR signal."""
|
||||
self._trigger_event("unknown", {"raw_code": signal.timings})
|
||||
self.async_write_ha_state()
|
||||
|
||||
remove_signal_subscription: CALLBACK_TYPE | None = None
|
||||
|
||||
@callback
|
||||
def _async_unsubscribe_receiver() -> None:
|
||||
"""Unsubscribe from the current IR receiver."""
|
||||
nonlocal remove_signal_subscription
|
||||
|
||||
if remove_signal_subscription is None:
|
||||
return
|
||||
remove_signal_subscription()
|
||||
remove_signal_subscription = None
|
||||
|
||||
@callback
|
||||
def _async_update_receiver_subscription(write_state: bool = True) -> None:
|
||||
"""Update the IR receiver subscription when availability changes."""
|
||||
nonlocal remove_signal_subscription
|
||||
|
||||
ir_state = self.hass.states.get(self._receiver_entity_id)
|
||||
receiver_available = (
|
||||
ir_state is not None and ir_state.state != STATE_UNAVAILABLE
|
||||
)
|
||||
|
||||
if not receiver_available:
|
||||
_async_unsubscribe_receiver()
|
||||
elif remove_signal_subscription is None:
|
||||
remove_signal_subscription = async_subscribe_receiver(
|
||||
self.hass, self._receiver_entity_id, _handle_signal
|
||||
)
|
||||
|
||||
if self._attr_available == receiver_available:
|
||||
return
|
||||
|
||||
self._attr_available = receiver_available
|
||||
if write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_ir_state_changed(event: Event[EventStateChangedData]) -> None:
|
||||
"""Handle infrared entity state changes."""
|
||||
_async_update_receiver_subscription()
|
||||
|
||||
_async_update_receiver_subscription(write_state=False)
|
||||
self.async_on_remove(_async_unsubscribe_receiver)
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [self._receiver_entity_id], _async_ir_state_changed
|
||||
)
|
||||
)
|
||||
@@ -3,16 +3,27 @@
|
||||
import infrared_protocols
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.components.infrared import InfraredEntity
|
||||
from homeassistant.components.infrared import (
|
||||
InfraredEmitterEntity,
|
||||
InfraredReceivedSignal,
|
||||
InfraredReceiverEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
INFRARED_COMMAND_SIGNAL = f"{DOMAIN}_infrared_command_signal"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -22,37 +33,60 @@ async def async_setup_entry(
|
||||
"""Set up the demo infrared platform."""
|
||||
async_add_entities(
|
||||
[
|
||||
DemoInfrared(
|
||||
unique_id="ir_transmitter",
|
||||
device_name="IR Blaster",
|
||||
entity_name="Infrared Transmitter",
|
||||
DemoInfraredEmitter(
|
||||
unique_id="ir_emitter",
|
||||
entity_name="Infrared Emitter",
|
||||
),
|
||||
DemoInfraredReceiver(
|
||||
unique_id="ir_receiver",
|
||||
entity_name="Infrared Receiver",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class DemoInfrared(InfraredEntity):
|
||||
# pylint: disable=hass-enforce-class-module
|
||||
class DemoInfraredEntityBase(Entity):
|
||||
"""Representation of a demo infrared entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
device_name: str,
|
||||
entity_name: str,
|
||||
) -> None:
|
||||
def __init__(self, unique_id: str, entity_name: str) -> None:
|
||||
"""Initialize the demo infrared entity."""
|
||||
super().__init__()
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
name=device_name,
|
||||
identifiers={(DOMAIN, "infrared")}, name="IR Blaster"
|
||||
)
|
||||
self._attr_name = entity_name
|
||||
|
||||
|
||||
class DemoInfraredEmitter(DemoInfraredEntityBase, InfraredEmitterEntity):
|
||||
"""Representation of a demo infrared emitter entity."""
|
||||
|
||||
async def async_send_command(self, command: infrared_protocols.Command) -> None:
|
||||
"""Send an IR command."""
|
||||
raw_timings = command.get_raw_timings()
|
||||
persistent_notification.async_create(
|
||||
self.hass, str(command.get_raw_timings()), title="Infrared Command"
|
||||
self.hass, str(raw_timings), title="Infrared Command Sent"
|
||||
)
|
||||
async_dispatcher_send(self.hass, INFRARED_COMMAND_SIGNAL, raw_timings)
|
||||
|
||||
|
||||
class DemoInfraredReceiver(DemoInfraredEntityBase, InfraredReceiverEntity):
|
||||
"""Representation of a demo infrared receiver entity."""
|
||||
|
||||
@callback
|
||||
def _on_dispatcher_signal(self, raw_timings: list[int]) -> None:
|
||||
"""Handle received infrared command signal."""
|
||||
self._handle_received_signal(InfraredReceivedSignal(timings=raw_timings))
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Called when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, INFRARED_COMMAND_SIGNAL, self._on_dispatcher_signal
|
||||
)
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"infrared_fan": {
|
||||
"abort": {
|
||||
"no_emitters": "No infrared transmitter entities found. Please set up an infrared device first."
|
||||
"no_emitters": "No infrared emitter entities found. Please set up an infrared device first."
|
||||
},
|
||||
"entry_type": "Infrared fan",
|
||||
"initiate_flow": {
|
||||
@@ -44,10 +44,11 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"infrared_entity_id": "Infrared transmitter",
|
||||
"infrared_entity_id": "Infrared emitter",
|
||||
"infrared_receiver_entity_id": "Infrared receiver",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"description": "Select an infrared transmitter to control the fan."
|
||||
"description": "Select an infrared emitter to control the fan."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from pysmlight.exceptions import SmlightError
|
||||
from pysmlight.models import IRPayload
|
||||
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEmitterEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -27,8 +27,8 @@ async def async_setup_entry(
|
||||
async_add_entities([SmInfraredEntity(coordinator)])
|
||||
|
||||
|
||||
class SmInfraredEntity(SmEntity, InfraredEntity):
|
||||
"""Representation of a SLZB-Ultima infrared."""
|
||||
class SmInfraredEntity(SmEntity, InfraredEmitterEntity):
|
||||
"""Representation of a SLZB-Ultima infrared emitter."""
|
||||
|
||||
_attr_translation_key = "infrared_emitter"
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
from infrared_protocols import Command as InfraredCommand
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.infrared import InfraredEntity
|
||||
from homeassistant.components.infrared import (
|
||||
InfraredEmitterEntity,
|
||||
InfraredReceiverEntity,
|
||||
)
|
||||
from homeassistant.components.infrared.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -16,14 +19,15 @@ async def init_integration(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
class MockInfraredEntity(InfraredEntity):
|
||||
"""Mock infrared entity for testing."""
|
||||
class MockInfraredEmitterEntity(InfraredEmitterEntity):
|
||||
"""Mock infrared emitter entity for testing."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Test IR transmitter"
|
||||
_attr_name = "Test IR emitter"
|
||||
|
||||
def __init__(self, unique_id: str) -> None:
|
||||
"""Initialize mock entity."""
|
||||
super().__init__()
|
||||
self._attr_unique_id = unique_id
|
||||
self.send_command_calls: list[InfraredCommand] = []
|
||||
|
||||
@@ -33,6 +37,23 @@ class MockInfraredEntity(InfraredEntity):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_infrared_entity() -> MockInfraredEntity:
|
||||
"""Return a mock infrared entity."""
|
||||
return MockInfraredEntity("test_ir_transmitter")
|
||||
def mock_infrared_emitter_entity() -> MockInfraredEmitterEntity:
|
||||
"""Return a mock infrared emitter entity."""
|
||||
return MockInfraredEmitterEntity("test_ir_emitter")
|
||||
|
||||
|
||||
class MockInfraredReceiverEntity(InfraredReceiverEntity):
|
||||
"""Mock infrared receiver entity for testing."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Test IR receiver"
|
||||
|
||||
def __init__(self, unique_id: str) -> None:
|
||||
"""Initialize mock receiver entity."""
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_infrared_receiver_entity() -> MockInfraredReceiverEntity:
|
||||
"""Return a mock infrared receiver entity."""
|
||||
return MockInfraredReceiverEntity("test_ir_receiver")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for the Infrared integration setup."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from infrared_protocols import NECCommand
|
||||
@@ -9,8 +9,11 @@ import pytest
|
||||
from homeassistant.components.infrared import (
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
InfraredReceivedSignal,
|
||||
async_get_emitters,
|
||||
async_get_receivers,
|
||||
async_send_command,
|
||||
async_subscribe_receiver,
|
||||
)
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
@@ -18,57 +21,79 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import MockInfraredEntity
|
||||
from .conftest import MockInfraredEmitterEntity, MockInfraredReceiverEntity
|
||||
|
||||
from tests.common import mock_restore_cache
|
||||
|
||||
TEST_COMMAND = NECCommand(address=0x04FB, command=0x08F7, modulation=38000)
|
||||
|
||||
async def test_get_entities_integration_setup(hass: HomeAssistant) -> None:
|
||||
"""Test getting entities when the integration is not setup."""
|
||||
|
||||
async def test_get_entities_component_not_loaded(hass: HomeAssistant) -> None:
|
||||
"""Test getting entities when the component is not loaded."""
|
||||
assert async_get_emitters(hass) == []
|
||||
assert async_get_receivers(hass) == []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_get_entities_empty(hass: HomeAssistant) -> None:
|
||||
"""Test getting entities when none are registered."""
|
||||
assert async_get_emitters(hass) == []
|
||||
assert async_get_receivers(hass) == []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_infrared_entity_initial_state(
|
||||
hass: HomeAssistant, mock_infrared_entity: MockInfraredEntity
|
||||
async def test_get_entities_filters_by_type(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test infrared entity has no state before any command is sent."""
|
||||
"""Test get_emitters/get_receivers return only entities of the matching type."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_entity])
|
||||
await component.async_add_entities(
|
||||
[mock_infrared_emitter_entity, mock_infrared_receiver_entity]
|
||||
)
|
||||
|
||||
state = hass.states.get("infrared.test_ir_transmitter")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert async_get_emitters(hass) == [mock_infrared_emitter_entity.entity_id]
|
||||
assert async_get_receivers(hass) == [mock_infrared_receiver_entity.entity_id]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_infrared_entities_initial_state(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test infrared entities have no state before any command is sent."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities(
|
||||
[mock_infrared_emitter_entity, mock_infrared_receiver_entity]
|
||||
)
|
||||
|
||||
assert (emitter_state := hass.states.get("infrared.test_ir_emitter")) is not None
|
||||
assert emitter_state.state == STATE_UNKNOWN
|
||||
assert (receiver_state := hass.states.get("infrared.test_ir_receiver")) is not None
|
||||
assert receiver_state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_async_send_command_success(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_entity: MockInfraredEntity,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test sending command via async_send_command helper."""
|
||||
# Add the mock entity to the component
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_entity])
|
||||
await component.async_add_entities([mock_infrared_emitter_entity])
|
||||
|
||||
# Freeze time so we can verify the state update
|
||||
now = dt_util.utcnow()
|
||||
freezer.move_to(now)
|
||||
|
||||
command = NECCommand(address=0x04FB, command=0x08F7, modulation=38000)
|
||||
await async_send_command(hass, mock_infrared_entity.entity_id, command)
|
||||
await async_send_command(hass, mock_infrared_emitter_entity.entity_id, TEST_COMMAND)
|
||||
|
||||
assert len(mock_infrared_entity.send_command_calls) == 1
|
||||
assert mock_infrared_entity.send_command_calls[0] is command
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
assert mock_infrared_emitter_entity.send_command_calls[0] is TEST_COMMAND
|
||||
|
||||
state = hass.states.get("infrared.test_ir_transmitter")
|
||||
state = hass.states.get("infrared.test_ir_emitter")
|
||||
assert state is not None
|
||||
assert state.state == now.isoformat(timespec="milliseconds")
|
||||
|
||||
@@ -76,27 +101,26 @@ async def test_async_send_command_success(
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_async_send_command_error_does_not_update_state(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_entity: MockInfraredEntity,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test that state is not updated when async_send_command raises an error."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_entity])
|
||||
await component.async_add_entities([mock_infrared_emitter_entity])
|
||||
|
||||
state = hass.states.get("infrared.test_ir_transmitter")
|
||||
state = hass.states.get("infrared.test_ir_emitter")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
command = NECCommand(address=0x04FB, command=0x08F7, modulation=38000)
|
||||
|
||||
mock_infrared_entity.async_send_command = AsyncMock(
|
||||
mock_infrared_emitter_entity.async_send_command = AsyncMock(
|
||||
side_effect=HomeAssistantError("Transmission failed")
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="Transmission failed"):
|
||||
await async_send_command(hass, mock_infrared_entity.entity_id, command)
|
||||
await async_send_command(
|
||||
hass, mock_infrared_emitter_entity.entity_id, TEST_COMMAND
|
||||
)
|
||||
|
||||
# Verify state was not updated after the error
|
||||
state = hass.states.get("infrared.test_ir_transmitter")
|
||||
state = hass.states.get("infrared.test_ir_emitter")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
@@ -104,25 +128,35 @@ async def test_async_send_command_error_does_not_update_state(
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_async_send_command_entity_not_found(hass: HomeAssistant) -> None:
|
||||
"""Test async_send_command raises error when entity not found."""
|
||||
command = NECCommand(
|
||||
address=0x04FB, command=0x08F7, modulation=38000, repeat_count=1
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Infrared entity `infrared.nonexistent_entity` not found",
|
||||
):
|
||||
await async_send_command(hass, "infrared.nonexistent_entity", command)
|
||||
await async_send_command(hass, "infrared.nonexistent_entity", TEST_COMMAND)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_async_send_command_rejects_receiver(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test async_send_command rejects a receiver entity."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_receiver_entity])
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"Infrared entity `{mock_infrared_receiver_entity.entity_id}` not found",
|
||||
):
|
||||
await async_send_command(
|
||||
hass, mock_infrared_receiver_entity.entity_id, TEST_COMMAND
|
||||
)
|
||||
|
||||
|
||||
async def test_async_send_command_component_not_loaded(hass: HomeAssistant) -> None:
|
||||
"""Test async_send_command raises error when component not loaded."""
|
||||
command = NECCommand(
|
||||
address=0x04FB, command=0x08F7, modulation=38000, repeat_count=1
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError, match="component_not_loaded"):
|
||||
await async_send_command(hass, "infrared.some_entity", command)
|
||||
await async_send_command(hass, "infrared.some_entity", TEST_COMMAND)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -134,19 +168,167 @@ async def test_async_send_command_component_not_loaded(hass: HomeAssistant) -> N
|
||||
)
|
||||
async def test_infrared_entity_state_restore(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_entity: MockInfraredEntity,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
restored_value: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test infrared entity state restore."""
|
||||
mock_restore_cache(hass, [State("infrared.test_ir_transmitter", restored_value)])
|
||||
mock_restore_cache(
|
||||
hass,
|
||||
[
|
||||
State("infrared.test_ir_emitter", restored_value),
|
||||
State("infrared.test_ir_receiver", restored_value),
|
||||
],
|
||||
)
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_entity])
|
||||
await component.async_add_entities(
|
||||
[mock_infrared_emitter_entity, mock_infrared_receiver_entity]
|
||||
)
|
||||
|
||||
state = hass.states.get("infrared.test_ir_transmitter")
|
||||
assert (emitter_state := hass.states.get("infrared.test_ir_emitter")) is not None
|
||||
assert emitter_state.state == expected_state
|
||||
assert (receiver_state := hass.states.get("infrared.test_ir_receiver")) is not None
|
||||
assert receiver_state.state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_async_subscribe_receiver_success(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test subscribing to a receiver via async_subscribe_receiver helper."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_receiver_entity])
|
||||
|
||||
now = dt_util.utcnow()
|
||||
freezer.move_to(now)
|
||||
|
||||
signal_callback = Mock()
|
||||
unsubscribe = async_subscribe_receiver(
|
||||
hass, mock_infrared_receiver_entity.entity_id, signal_callback
|
||||
)
|
||||
|
||||
signal = InfraredReceivedSignal(timings=[100, 200, 300], modulation=38000)
|
||||
mock_infrared_receiver_entity._handle_received_signal(signal)
|
||||
|
||||
assert signal_callback.call_count == 1
|
||||
assert signal_callback.call_args[0][0] is signal
|
||||
|
||||
state = hass.states.get("infrared.test_ir_receiver")
|
||||
assert state is not None
|
||||
assert state.state == expected_state
|
||||
assert state.state == now.isoformat(timespec="milliseconds")
|
||||
|
||||
unsubscribe()
|
||||
mock_infrared_receiver_entity._handle_received_signal(signal)
|
||||
assert signal_callback.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_handle_received_signal_isolates_callback_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a failing subscriber does not prevent other subscribers from running."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_receiver_entity])
|
||||
|
||||
failing_callback = Mock(side_effect=RuntimeError("boom"))
|
||||
working_callback = Mock()
|
||||
async_subscribe_receiver(
|
||||
hass, mock_infrared_receiver_entity.entity_id, failing_callback
|
||||
)
|
||||
async_subscribe_receiver(
|
||||
hass, mock_infrared_receiver_entity.entity_id, working_callback
|
||||
)
|
||||
|
||||
signal = InfraredReceivedSignal(timings=[100, 200, 300])
|
||||
mock_infrared_receiver_entity._handle_received_signal(signal)
|
||||
|
||||
failing_callback.assert_called_once_with(signal)
|
||||
working_callback.assert_called_once_with(signal)
|
||||
assert "Error in signal callback" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_handle_received_signal_unsubscribe_during_dispatch(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test a subscriber can unsubscribe itself during dispatch without error."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_receiver_entity])
|
||||
|
||||
other_callback = Mock()
|
||||
|
||||
def unsubscribing_callback(signal: InfraredReceivedSignal) -> None:
|
||||
unsubscribe()
|
||||
|
||||
self_unsub_mock = Mock(side_effect=unsubscribing_callback)
|
||||
unsubscribe = async_subscribe_receiver(
|
||||
hass, mock_infrared_receiver_entity.entity_id, self_unsub_mock
|
||||
)
|
||||
async_subscribe_receiver(
|
||||
hass, mock_infrared_receiver_entity.entity_id, other_callback
|
||||
)
|
||||
|
||||
signal = InfraredReceivedSignal(timings=[100, 200, 300])
|
||||
mock_infrared_receiver_entity._handle_received_signal(signal)
|
||||
|
||||
self_unsub_mock.assert_called_once_with(signal)
|
||||
other_callback.assert_called_once_with(signal)
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(signal)
|
||||
self_unsub_mock.assert_called_once_with(signal)
|
||||
assert other_callback.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
@pytest.mark.parametrize(
|
||||
"entity_id_or_uuid",
|
||||
["infrared.nonexistent_entity", "invalid-id"],
|
||||
)
|
||||
async def test_async_subscribe_receiver_not_found(
|
||||
hass: HomeAssistant, entity_id_or_uuid: str
|
||||
) -> None:
|
||||
"""Test async_subscribe_receiver raises when the entity is missing or invalid."""
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"Infrared receiver entity `{entity_id_or_uuid}` not found",
|
||||
):
|
||||
async_subscribe_receiver(hass, entity_id_or_uuid, lambda _: None)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_async_subscribe_receiver_rejects_emitter(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test async_subscribe_receiver rejects an emitter entity."""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_emitter_entity])
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=(
|
||||
f"Infrared receiver entity `{mock_infrared_emitter_entity.entity_id}`"
|
||||
" not found"
|
||||
),
|
||||
):
|
||||
async_subscribe_receiver(
|
||||
hass, mock_infrared_emitter_entity.entity_id, lambda _: None
|
||||
)
|
||||
|
||||
|
||||
async def test_async_subscribe_receiver_component_not_loaded(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_subscribe_receiver raises error when component not loaded."""
|
||||
with pytest.raises(HomeAssistantError, match="component_not_loaded"):
|
||||
async_subscribe_receiver(hass, "infrared.some_entity", lambda _: None)
|
||||
|
||||
@@ -14,7 +14,8 @@ from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ENTITY_IR_TRANSMITTER = "infrared.ir_blaster_infrared_transmitter"
|
||||
ENTITY_IR_EMITTER = "infrared.ir_blaster_infrared_emitter"
|
||||
ENTITY_IR_RECEIVER = "infrared.ir_blaster_infrared_receiver"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -227,7 +228,8 @@ async def test_infrared_fan_subentry_flow(hass: HomeAssistant) -> None:
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
"name": "Living Room Fan",
|
||||
"infrared_entity_id": ENTITY_IR_TRANSMITTER,
|
||||
"infrared_entity_id": ENTITY_IR_EMITTER,
|
||||
"infrared_receiver_entity_id": ENTITY_IR_RECEIVER,
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
@@ -237,7 +239,10 @@ async def test_infrared_fan_subentry_flow(hass: HomeAssistant) -> None:
|
||||
if s.subentry_type == "infrared_fan"
|
||||
][0]
|
||||
assert config_entry.subentries[subentry_id] == config_entries.ConfigSubentry(
|
||||
data={"infrared_entity_id": ENTITY_IR_TRANSMITTER},
|
||||
data={
|
||||
"infrared_entity_id": ENTITY_IR_EMITTER,
|
||||
"infrared_receiver_entity_id": ENTITY_IR_RECEIVER,
|
||||
},
|
||||
subentry_id=subentry_id,
|
||||
subentry_type="infrared_fan",
|
||||
title="Living Room Fan",
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
"""The tests for the kitchen_sink infrared platform."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import infrared_protocols
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.infrared import async_send_command
|
||||
from homeassistant.components.infrared import (
|
||||
async_send_command,
|
||||
async_subscribe_receiver,
|
||||
)
|
||||
from homeassistant.components.kitchen_sink import DOMAIN
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
ENTITY_IR_TRANSMITTER = "infrared.ir_blaster_infrared_transmitter"
|
||||
ENTITY_IR_EMITTER = "infrared.ir_blaster_infrared_emitter"
|
||||
ENTITY_IR_RECEIVER = "infrared.ir_blaster_infrared_receiver"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -33,13 +37,12 @@ async def setup_comp(hass: HomeAssistant, infrared_only: None) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_send_command(
|
||||
async def test_send_receive(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
||||
) -> None:
|
||||
"""Test sending an infrared command."""
|
||||
state = hass.states.get(ENTITY_IR_TRANSMITTER)
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
"""Test the receiver picks up commands sent by the emitter via dispatcher."""
|
||||
signal_callback = Mock()
|
||||
async_subscribe_receiver(hass, ENTITY_IR_RECEIVER, signal_callback)
|
||||
|
||||
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
|
||||
assert now is not None
|
||||
@@ -48,8 +51,13 @@ async def test_send_command(
|
||||
command = infrared_protocols.NECCommand(
|
||||
address=0x04, command=0x08, modulation=38000
|
||||
)
|
||||
await async_send_command(hass, ENTITY_IR_TRANSMITTER, command)
|
||||
await async_send_command(hass, ENTITY_IR_EMITTER, command)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_IR_TRANSMITTER)
|
||||
state = hass.states.get(ENTITY_IR_RECEIVER)
|
||||
assert state
|
||||
assert state.state == now.isoformat(timespec="milliseconds")
|
||||
|
||||
assert signal_callback.call_count == 1
|
||||
received_signal = signal_callback.call_args[0][0]
|
||||
assert received_signal.timings == command.get_raw_timings()
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from homeassistant.components.infrared import (
|
||||
DATA_COMPONENT as INFRARED_DATA_COMPONENT,
|
||||
DOMAIN as INFRARED_DOMAIN,
|
||||
InfraredEntity,
|
||||
InfraredEmitterEntity,
|
||||
)
|
||||
from homeassistant.components.lg_infrared import PLATFORMS
|
||||
from homeassistant.components.lg_infrared.const import (
|
||||
@@ -27,8 +27,8 @@ from tests.common import MockConfigEntry
|
||||
MOCK_INFRARED_ENTITY_ID = "infrared.test_ir_transmitter"
|
||||
|
||||
|
||||
class MockInfraredEntity(InfraredEntity):
|
||||
"""Mock infrared entity for testing."""
|
||||
class MockInfraredEntity(InfraredEmitterEntity):
|
||||
"""Mock infrared emitter entity for testing."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Test IR transmitter"
|
||||
|
||||
Reference in New Issue
Block a user