mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 22:38:02 -04:00
Add Osram Infrared integration (#173250)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
Abílio Costa
parent
e71ec7228a
commit
c8ec9151ad
@@ -451,6 +451,7 @@ homeassistant.components.openuv.*
|
||||
homeassistant.components.opnsense.*
|
||||
homeassistant.components.opower.*
|
||||
homeassistant.components.oralb.*
|
||||
homeassistant.components.osram_infrared.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.otp.*
|
||||
homeassistant.components.ouman_eh_800.*
|
||||
|
||||
Generated
+2
@@ -1396,6 +1396,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/oralb/ @bdraco @Lash-L
|
||||
/homeassistant/components/osoenergy/ @osohotwateriot
|
||||
/tests/components/osoenergy/ @osohotwateriot
|
||||
/homeassistant/components/osram_infrared/ @freeDom-
|
||||
/tests/components/osram_infrared/ @freeDom-
|
||||
/homeassistant/components/otbr/ @home-assistant/core
|
||||
/tests/components/otbr/ @home-assistant/core
|
||||
/homeassistant/components/ouman_eh_800/ @Markus98
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""The OSRAM infrared integration."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.LIGHT,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up OSRAM 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 an OSRAM IR config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Config flow for OSRAM Infrared."""
|
||||
|
||||
from collections.abc import Collection
|
||||
from typing import Any, override
|
||||
|
||||
import probatio
|
||||
|
||||
from homeassistant.components.infrared import (
|
||||
DOMAIN as INFRARED_DOMAIN,
|
||||
async_get_emitters,
|
||||
async_get_receivers,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig
|
||||
|
||||
from .const import CONF_IR_EMITTER_ENTITY_ID, CONF_IR_RECEIVER_ENTITY_ID, DOMAIN
|
||||
|
||||
|
||||
class OsramIrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for OSRAM Infrared."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def _async_get_entry_title(self, emitter_entity_id: str) -> str:
|
||||
"""Return config entry title for the selected emitter."""
|
||||
entity_registry = er.async_get(self.hass)
|
||||
entity_entry = entity_registry.async_get(emitter_entity_id)
|
||||
|
||||
if entity_entry is None:
|
||||
emitter_name = emitter_entity_id
|
||||
else:
|
||||
emitter_name = (
|
||||
entity_entry.name
|
||||
or entity_entry.original_name
|
||||
or entity_entry.entity_id
|
||||
)
|
||||
|
||||
return f"OSRAM light via {emitter_name}"
|
||||
|
||||
def _async_validate_input(
|
||||
self,
|
||||
user_input: dict[str, Any],
|
||||
emitter_entity_ids: Collection[str],
|
||||
receiver_entity_ids: Collection[str],
|
||||
) -> dict[str, str]:
|
||||
"""Validate the user input."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
emitter_entity_id = user_input[CONF_IR_EMITTER_ENTITY_ID]
|
||||
if emitter_entity_id not in emitter_entity_ids:
|
||||
errors[CONF_IR_EMITTER_ENTITY_ID] = "entity_not_available"
|
||||
|
||||
if (
|
||||
receiver_entity_id := user_input.get(CONF_IR_RECEIVER_ENTITY_ID)
|
||||
) and receiver_entity_id not in receiver_entity_ids:
|
||||
errors[CONF_IR_RECEIVER_ENTITY_ID] = "entity_not_available"
|
||||
|
||||
return errors
|
||||
|
||||
@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)
|
||||
receiver_entity_ids = async_get_receivers(self.hass)
|
||||
|
||||
if not emitter_entity_ids:
|
||||
return self.async_abort(reason="no_infrared_emitters")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
emitter_entity_id = user_input[CONF_IR_EMITTER_ENTITY_ID]
|
||||
|
||||
self._async_abort_entries_match(
|
||||
{CONF_IR_EMITTER_ENTITY_ID: emitter_entity_id}
|
||||
)
|
||||
|
||||
errors = self._async_validate_input(
|
||||
user_input,
|
||||
emitter_entity_ids,
|
||||
receiver_entity_ids,
|
||||
)
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._async_get_entry_title(emitter_entity_id),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=probatio.Schema(
|
||||
{
|
||||
probatio.Required(CONF_IR_EMITTER_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=emitter_entity_ids,
|
||||
)
|
||||
),
|
||||
probatio.Optional(CONF_IR_RECEIVER_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=receiver_entity_ids,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Constants for the OSRAM infrared integration."""
|
||||
|
||||
DOMAIN = "osram_infrared"
|
||||
|
||||
CONF_IR_EMITTER_ENTITY_ID = "infrared_emitter_entity_id"
|
||||
CONF_IR_RECEIVER_ENTITY_ID = "infrared_receiver_entity_id"
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Common entities for the OSRAM IR integration."""
|
||||
|
||||
from asyncio import sleep
|
||||
|
||||
from infrared_protocols.codes.osram.light import OsramLightCode
|
||||
|
||||
from homeassistant.components.infrared import InfraredEmitterConsumerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
FULL_FRAME_REPEAT_DELAY = 0.01
|
||||
|
||||
|
||||
class OsramIrEntity(Entity):
|
||||
"""OSRAM IR base entity providing common device info."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, entry: ConfigEntry) -> None:
|
||||
"""Initialize an OSRAM IR entity."""
|
||||
self._attr_unique_id = entry.entry_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
name="OSRAM light",
|
||||
manufacturer="OSRAM",
|
||||
)
|
||||
|
||||
|
||||
class OsramIrEmitterEntity(OsramIrEntity, InfraredEmitterConsumerEntity):
|
||||
"""Base entity that sends commands through an infrared emitter."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
emitter_entity_id: str,
|
||||
) -> None:
|
||||
"""Initialize an OSRAM IR emitter consumer entity."""
|
||||
super().__init__(entry)
|
||||
self._infrared_emitter_entity_id = emitter_entity_id
|
||||
|
||||
async def _async_send_code(
|
||||
self,
|
||||
code: OsramLightCode,
|
||||
*,
|
||||
repeat_count: int = 1,
|
||||
) -> None:
|
||||
"""Send an OSRAM command one or more times as complete NEC frames."""
|
||||
for frame_index in range(repeat_count):
|
||||
if frame_index:
|
||||
await sleep(FULL_FRAME_REPEAT_DELAY)
|
||||
|
||||
await self._send_command(code.to_command())
|
||||
@@ -0,0 +1,329 @@
|
||||
"""Light platform for the OSRAM infrared integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any, Final, override
|
||||
|
||||
from infrared_protocols.codes.osram.light import OSRAM_ADDRESS, OsramLightCode
|
||||
from infrared_protocols.commands.nec import NECCommand
|
||||
|
||||
from homeassistant.components.infrared import (
|
||||
InfraredReceivedSignal,
|
||||
InfraredReceiverConsumerEntity,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_EFFECT,
|
||||
ATTR_RGB_COLOR,
|
||||
EFFECT_OFF,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from .const import CONF_IR_EMITTER_ENTITY_ID, CONF_IR_RECEIVER_ENTITY_ID
|
||||
from .entity import OsramIrEmitterEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
# Use the dedicated white command for colors with low saturation.
|
||||
WHITE_SATURATION_THRESHOLD = 45.0
|
||||
|
||||
RGB_WHITE: Final[tuple[int, int, int]] = (255, 255, 255)
|
||||
|
||||
# The physical remote exposes 15 discrete color presets.
|
||||
HUE_TO_CODE: Final[dict[int, OsramLightCode]] = {
|
||||
0: OsramLightCode.HUE_000,
|
||||
15: OsramLightCode.HUE_015,
|
||||
30: OsramLightCode.HUE_030,
|
||||
45: OsramLightCode.HUE_045,
|
||||
60: OsramLightCode.HUE_060,
|
||||
120: OsramLightCode.HUE_120,
|
||||
135: OsramLightCode.HUE_135,
|
||||
150: OsramLightCode.HUE_150,
|
||||
165: OsramLightCode.HUE_165,
|
||||
180: OsramLightCode.HUE_180,
|
||||
240: OsramLightCode.HUE_240,
|
||||
255: OsramLightCode.HUE_255,
|
||||
270: OsramLightCode.HUE_270,
|
||||
285: OsramLightCode.HUE_285,
|
||||
300: OsramLightCode.HUE_300,
|
||||
}
|
||||
|
||||
SUPPORTED_HUES: Final[tuple[int, ...]] = (*HUE_TO_CODE, 360)
|
||||
|
||||
CODE_TO_HUE: Final[dict[OsramLightCode, int]] = {
|
||||
code: hue for hue, code in HUE_TO_CODE.items()
|
||||
}
|
||||
|
||||
CODE_TO_RGB: Final[dict[OsramLightCode, tuple[int, int, int]]] = {
|
||||
OsramLightCode.WHITE: RGB_WHITE,
|
||||
**{
|
||||
code: color_util.color_hs_to_RGB(hue, 100.0)
|
||||
for code, hue in CODE_TO_HUE.items()
|
||||
},
|
||||
}
|
||||
|
||||
EFFECT_TO_CODE: Final[dict[str, OsramLightCode]] = {
|
||||
"flash": OsramLightCode.FLASH,
|
||||
"strobe": OsramLightCode.STROBE,
|
||||
"smooth": OsramLightCode.SMOOTH,
|
||||
}
|
||||
|
||||
CODE_TO_EFFECT: Final[dict[OsramLightCode, str]] = {
|
||||
code: effect for effect, code in EFFECT_TO_CODE.items()
|
||||
}
|
||||
|
||||
EFFECT_LIST: Final[list[str]] = [
|
||||
EFFECT_OFF,
|
||||
*EFFECT_TO_CODE,
|
||||
]
|
||||
|
||||
CMD_REPEAT_COUNT = 3
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up an OSRAM infrared light from a config entry."""
|
||||
emitter_entity_id = entry.data[CONF_IR_EMITTER_ENTITY_ID]
|
||||
if receiver_entity_id := entry.data.get(CONF_IR_RECEIVER_ENTITY_ID):
|
||||
async_add_entities(
|
||||
[
|
||||
OsramIrLightWithReceiver(
|
||||
entry,
|
||||
emitter_entity_id,
|
||||
receiver_entity_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
async_add_entities(
|
||||
[
|
||||
OsramIrLight(
|
||||
entry,
|
||||
emitter_entity_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _snap_hue(hue: float) -> int:
|
||||
"""Snap an arbitrary hue to the nearest physical remote preset."""
|
||||
normalized_hue = hue % 360
|
||||
|
||||
# 360° is included as an alias for 0° to handle the wrap-around at red.
|
||||
return (
|
||||
min(
|
||||
SUPPORTED_HUES,
|
||||
key=lambda supported_hue: abs(normalized_hue - supported_hue),
|
||||
)
|
||||
% 360
|
||||
)
|
||||
|
||||
|
||||
def _rgb_color_to_code_and_reported_rgb(
|
||||
rgb_color: tuple[int, int, int],
|
||||
) -> tuple[OsramLightCode, tuple[int, int, int]]:
|
||||
"""Return nearest OSRAM command and reported RGB color for an RGB color."""
|
||||
hue, saturation = color_util.color_RGB_to_hs(*rgb_color)
|
||||
|
||||
if saturation <= WHITE_SATURATION_THRESHOLD:
|
||||
return OsramLightCode.WHITE, RGB_WHITE
|
||||
|
||||
snapped_hue = _snap_hue(hue)
|
||||
code = HUE_TO_CODE[snapped_hue]
|
||||
|
||||
return code, CODE_TO_RGB[code]
|
||||
|
||||
|
||||
class OsramIrLight(OsramIrEmitterEntity, LightEntity):
|
||||
"""Representation of an OSRAM infrared light."""
|
||||
|
||||
_attr_assumed_state = True
|
||||
_attr_color_mode = ColorMode.RGB
|
||||
_attr_effect_list = EFFECT_LIST
|
||||
_attr_name = None
|
||||
_attr_rgb_color = RGB_WHITE
|
||||
_attr_supported_color_modes = {ColorMode.RGB}
|
||||
_attr_supported_features = LightEntityFeature.EFFECT
|
||||
|
||||
def __init__(self, entry: ConfigEntry, emitter_entity_id: str) -> None:
|
||||
"""Initialize an OSRAM infrared light."""
|
||||
super().__init__(entry, emitter_entity_id)
|
||||
|
||||
self._attr_is_on = None
|
||||
self._attr_effect = EFFECT_OFF
|
||||
self._last_static_color_code = OsramLightCode.WHITE
|
||||
self._last_static_rgb_color = RGB_WHITE
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light and apply optional effect and color."""
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
|
||||
if effect is not None and effect not in EFFECT_LIST:
|
||||
raise ServiceValidationError(f"Unsupported OSRAM infrared effect: {effect}")
|
||||
|
||||
await self._async_send_code(
|
||||
OsramLightCode.ON,
|
||||
repeat_count=CMD_REPEAT_COUNT,
|
||||
)
|
||||
|
||||
if effect is not None:
|
||||
await self._async_set_effect(effect)
|
||||
elif (rgb_color := kwargs.get(ATTR_RGB_COLOR)) is not None:
|
||||
await self._async_set_rgb_color(rgb_color)
|
||||
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self._async_send_code(
|
||||
OsramLightCode.OFF,
|
||||
repeat_count=CMD_REPEAT_COUNT,
|
||||
)
|
||||
|
||||
self._update_off_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_set_rgb_color(
|
||||
self,
|
||||
rgb_color: tuple[int, int, int],
|
||||
) -> None:
|
||||
"""Set the nearest supported static color preset."""
|
||||
code, reported_rgb_color = _rgb_color_to_code_and_reported_rgb(rgb_color)
|
||||
|
||||
await self._async_send_code(
|
||||
code,
|
||||
repeat_count=CMD_REPEAT_COUNT,
|
||||
)
|
||||
|
||||
self._update_static_color_state(code, reported_rgb_color)
|
||||
|
||||
async def _async_set_effect(self, effect: str) -> None:
|
||||
"""Start or stop a native OSRAM effect."""
|
||||
if effect not in EFFECT_LIST:
|
||||
raise ServiceValidationError(f"Unsupported OSRAM infrared effect: {effect}")
|
||||
|
||||
if effect == EFFECT_OFF:
|
||||
await self._async_send_code(
|
||||
self._last_static_color_code,
|
||||
repeat_count=CMD_REPEAT_COUNT,
|
||||
)
|
||||
|
||||
self._update_static_color_state(
|
||||
self._last_static_color_code,
|
||||
self._last_static_rgb_color,
|
||||
)
|
||||
return
|
||||
|
||||
await self._async_send_code(
|
||||
EFFECT_TO_CODE[effect],
|
||||
repeat_count=CMD_REPEAT_COUNT,
|
||||
)
|
||||
self._update_effect_state(effect)
|
||||
|
||||
@callback
|
||||
def _update_off_state(self) -> None:
|
||||
"""Update the local state after an off command."""
|
||||
self._attr_is_on = False
|
||||
self._attr_effect = EFFECT_OFF
|
||||
|
||||
@callback
|
||||
def _update_static_color_state(
|
||||
self,
|
||||
code: OsramLightCode,
|
||||
rgb_color: tuple[int, int, int],
|
||||
) -> None:
|
||||
"""Update the local state after selecting a static color."""
|
||||
self._attr_is_on = True
|
||||
self._attr_effect = EFFECT_OFF
|
||||
self._attr_color_mode = ColorMode.RGB
|
||||
self._attr_rgb_color = rgb_color
|
||||
self._last_static_color_code = code
|
||||
self._last_static_rgb_color = rgb_color
|
||||
|
||||
@callback
|
||||
def _update_effect_state(self, effect: str) -> None:
|
||||
"""Update the local state after selecting an effect."""
|
||||
self._attr_is_on = True
|
||||
self._attr_effect = effect
|
||||
self._attr_color_mode = ColorMode.RGB
|
||||
|
||||
|
||||
class OsramIrLightWithReceiver(OsramIrLight, InfraredReceiverConsumerEntity):
|
||||
"""Representation of an OSRAM infrared light with a configured receiver."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
emitter_entity_id: str,
|
||||
receiver_entity_id: str,
|
||||
) -> None:
|
||||
"""Initialize an OSRAM infrared light."""
|
||||
super().__init__(entry, emitter_entity_id)
|
||||
|
||||
self._infrared_receiver_entity_id = receiver_entity_id
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _handle_signal(self, signal: InfraredReceivedSignal) -> None:
|
||||
"""Update the assumed light state after receiving an OSRAM command."""
|
||||
nec_command = NECCommand.from_raw_timings(signal.timings)
|
||||
|
||||
if nec_command is None or nec_command.address != OSRAM_ADDRESS:
|
||||
return
|
||||
|
||||
try:
|
||||
code = OsramLightCode(nec_command.command)
|
||||
except ValueError:
|
||||
_LOGGER.debug(
|
||||
"Ignoring unknown OSRAM infrared command: 0x%02X",
|
||||
nec_command.command,
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"Received OSRAM infrared command: %s (0x%02X)",
|
||||
code.name,
|
||||
nec_command.command,
|
||||
)
|
||||
|
||||
self._apply_received_code(code)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _apply_received_code(self, code: OsramLightCode) -> None:
|
||||
"""Apply a received infrared command without transmitting anything."""
|
||||
if code is OsramLightCode.OFF:
|
||||
self._update_off_state()
|
||||
return
|
||||
|
||||
# Every other recognized command requires the bulb to be powered on.
|
||||
self._attr_is_on = True
|
||||
|
||||
if code is OsramLightCode.ON:
|
||||
return
|
||||
|
||||
if (rgb_color := CODE_TO_RGB.get(code)) is not None:
|
||||
self._update_static_color_state(code, rgb_color)
|
||||
return
|
||||
|
||||
if (effect := CODE_TO_EFFECT.get(code)) is not None:
|
||||
self._update_effect_state(effect)
|
||||
return
|
||||
|
||||
if code is OsramLightCode.MODE:
|
||||
# MODE changes color of the bulb in a predefined order but
|
||||
# does not map reliably to a Home Assistant light effect.
|
||||
return
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "osram_infrared",
|
||||
"name": "OSRAM Infrared",
|
||||
"codeowners": ["@freeDom-"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["infrared"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/osram_infrared",
|
||||
"integration_type": "device",
|
||||
"iot_class": "assumed_state",
|
||||
"quality_scale": "silver"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional service actions.
|
||||
Light functionality is exposed through the standard light entity 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: The integration does not provide custom automation conditions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
docs-triggers:
|
||||
status: exempt
|
||||
comment: The integration does not provide custom automation 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: done
|
||||
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 provide additional service 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 authenticate with a device or service.
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics:
|
||||
status: todo
|
||||
comment: |
|
||||
Evaluate whether diagnostics add useful information for this
|
||||
assumed-state infrared integration.
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use discovery.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration is configured manually by selecting an existing infrared
|
||||
emitter entity.
|
||||
docs-data-update:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not fetch data from devices.
|
||||
docs-examples:
|
||||
status: todo
|
||||
comment: |
|
||||
Add example automations for setting a color, brightness, and an effect.
|
||||
docs-known-limitations:
|
||||
status: todo
|
||||
comment: |
|
||||
Document assumed state, possible state drift when using the physical
|
||||
remote, discrete hue presets, and approximate relative brightness
|
||||
tracking.
|
||||
docs-supported-devices:
|
||||
status: todo
|
||||
comment: |
|
||||
Document the tested OSRAM LEDVANCE bulb models and any known unsupported
|
||||
variants.
|
||||
docs-supported-functions:
|
||||
status: todo
|
||||
comment: |
|
||||
Document on/off control, discrete HS colors, white mode, relative
|
||||
brightness, and native effects.
|
||||
docs-troubleshooting:
|
||||
status: todo
|
||||
comment: |
|
||||
Document emitter availability checks and common infrared transmission
|
||||
issues.
|
||||
docs-use-cases:
|
||||
status: todo
|
||||
comment: |
|
||||
Describe replacing the original OSRAM infrared remote with a Home
|
||||
Assistant-controlled infrared emitter.
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Each config entry represents one manually configured infrared-controlled
|
||||
light. Devices are not discovered dynamically.
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration currently exposes only the light entity, which is not a
|
||||
configuration or diagnostic entity, so no entity category is required.
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: |
|
||||
Light entities do not require a device class.
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration currently exposes only the light entity, which should be
|
||||
enabled by default.
|
||||
entity-translations: done
|
||||
exception-translations:
|
||||
status: todo
|
||||
comment: |
|
||||
Ensure any user-facing HomeAssistantError exceptions use translation
|
||||
keys instead of hard-coded messages.
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not define custom entity icons.
|
||||
reconfiguration-flow:
|
||||
status: todo
|
||||
comment: |
|
||||
Add a reconfiguration flow so that the infrared emitter entity can be
|
||||
changed without removing and recreating the integration.
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
No conditions currently require repair issues.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Each config entry represents exactly one manually configured light
|
||||
device. There are no dynamically managed devices.
|
||||
|
||||
# Platinum
|
||||
async-dependency:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration has no external dependencies.
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not make HTTP requests.
|
||||
strict-typing: done
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This OSRAM light has already been configured with this infrared entity.",
|
||||
"no_infrared_emitters": "[%key:common::config_flow::abort::no_infrared_emitters%]"
|
||||
},
|
||||
"error": {
|
||||
"entity_not_available": "The selected infrared entity is no longer available. Select another entity."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"infrared_emitter_entity_id": "[%key:common::config_flow::data::infrared_entity_id%]",
|
||||
"infrared_receiver_entity_id": "[%key:common::config_flow::data::infrared_receiver_entity_id%]"
|
||||
},
|
||||
"data_description": {
|
||||
"infrared_emitter_entity_id": "[%key:common::config_flow::data_description::infrared_entity_id%]",
|
||||
"infrared_receiver_entity_id": "The optional infrared receiver entity used to detect commands sent by the physical remote."
|
||||
},
|
||||
"description": "Select an infrared emitter to use with your OSRAM light. Optionally select a receiver to track commands sent by the physical remote.",
|
||||
"title": "Set up OSRAM infrared light"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -591,6 +591,7 @@ FLOWS = {
|
||||
"oralb",
|
||||
"orvibo",
|
||||
"osoenergy",
|
||||
"osram_infrared",
|
||||
"otbr",
|
||||
"ouman_eh_800",
|
||||
"ourgroceries",
|
||||
|
||||
@@ -5396,6 +5396,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"osram_infrared": {
|
||||
"name": "OSRAM Infrared",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "assumed_state"
|
||||
},
|
||||
"osramlightify": {
|
||||
"name": "Osramlightify",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -4268,6 +4268,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.osram_infrared.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.otbr.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for the OSRAM infrared integration."""
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Common fixtures for the OSRAM infrared tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.osram_infrared import PLATFORMS
|
||||
from homeassistant.components.osram_infrared.const import (
|
||||
CONF_IR_EMITTER_ENTITY_ID,
|
||||
CONF_IR_RECEIVER_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,
|
||||
RECEIVER_ENTITY_ID as MOCK_INFRARED_RECEIVER_ENTITY_ID,
|
||||
)
|
||||
from tests.components.infrared.common import (
|
||||
MockInfraredEmitterEntity,
|
||||
MockInfraredReceiverEntity,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def has_receiver_entity() -> bool:
|
||||
"""Return whether the mock config entry has a receiver entity."""
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(has_receiver_entity: bool) -> MockConfigEntry:
|
||||
"""Return a mock OSRAM infrared config entry."""
|
||||
data = {
|
||||
CONF_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
}
|
||||
|
||||
if has_receiver_entity:
|
||||
data[CONF_IR_RECEIVER_ENTITY_ID] = MOCK_INFRARED_RECEIVER_ENTITY_ID
|
||||
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
entry_id="01JTEST0000000000000000000",
|
||||
title="OSRAM light via Test IR emitter",
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return PLATFORMS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_osram_light_code_to_command() -> Generator[None]:
|
||||
"""Patch OsramLightCode.to_command to return the OsramLightCode directly.
|
||||
|
||||
This allows tests to assert on the high-level code enum value rather than the
|
||||
raw NEC command timings.
|
||||
"""
|
||||
with patch(
|
||||
"infrared_protocols.codes.osram.light.OsramLightCode.to_command",
|
||||
autospec=True,
|
||||
side_effect=lambda self, **kwargs: self,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
mock_osram_light_code_to_command: None,
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the OSRAM infrared integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.osram_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,78 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[light.osram_light-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<LightEntityCapabilityAttribute.EFFECT_LIST: 'effect_list'>: list([
|
||||
'off',
|
||||
'flash',
|
||||
'strobe',
|
||||
'smooth',
|
||||
]),
|
||||
<LightEntityCapabilityAttribute.SUPPORTED_COLOR_MODES: 'supported_color_modes'>: list([
|
||||
<ColorMode.RGB: 'rgb'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.osram_light',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'osram_infrared',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <LightEntityFeature: 4>,
|
||||
'translation_key': None,
|
||||
'unique_id': '01JTEST0000000000000000000',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[light.osram_light-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.ASSUMED_STATE: 'assumed_state'>: True,
|
||||
<LightEntityStateAttribute.BRIGHTNESS: 'brightness'>: None,
|
||||
<LightEntityStateAttribute.COLOR_MODE: 'color_mode'>: None,
|
||||
<LightEntityStateAttribute.EFFECT: 'effect'>: None,
|
||||
<LightEntityCapabilityAttribute.EFFECT_LIST: 'effect_list'>: list([
|
||||
'off',
|
||||
'flash',
|
||||
'strobe',
|
||||
'smooth',
|
||||
]),
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'OSRAM light',
|
||||
<LightEntityStateAttribute.HS_COLOR: 'hs_color'>: None,
|
||||
<LightEntityStateAttribute.RGB_COLOR: 'rgb_color'>: None,
|
||||
<LightEntityCapabilityAttribute.SUPPORTED_COLOR_MODES: 'supported_color_modes'>: list([
|
||||
<ColorMode.RGB: 'rgb'>,
|
||||
]),
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <LightEntityFeature: 4>,
|
||||
<LightEntityStateAttribute.XY_COLOR: 'xy_color'>: None,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.osram_light',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,284 @@
|
||||
"""Tests for the OSRAM infrared config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.osram_infrared.const import (
|
||||
CONF_IR_EMITTER_ENTITY_ID,
|
||||
CONF_IR_RECEIVER_ENTITY_ID,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.infrared import (
|
||||
EMITTER_ENTITY_ID as MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
RECEIVER_ENTITY_ID as MOCK_INFRARED_RECEIVER_ENTITY_ID,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_infrared_emitter_entity",
|
||||
"mock_infrared_receiver_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_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
CONF_IR_RECEIVER_ENTITY_ID: MOCK_INFRARED_RECEIVER_ENTITY_ID,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "OSRAM light via Test IR emitter"
|
||||
assert result["data"] == {
|
||||
CONF_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
CONF_IR_RECEIVER_ENTITY_ID: MOCK_INFRARED_RECEIVER_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 emitter 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_IR_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 emitter exists."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_infrared_emitters"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
|
||||
@pytest.mark.parametrize(
|
||||
("entity_name", "expected_title"),
|
||||
[
|
||||
(None, "OSRAM light via Test IR emitter"),
|
||||
("Living room IR emitter", "OSRAM light via Living room IR emitter"),
|
||||
],
|
||||
)
|
||||
async def test_user_flow_title_from_entity_name(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_name: str | None,
|
||||
expected_title: str,
|
||||
) -> None:
|
||||
"""Test config-entry title uses the infrared emitter name."""
|
||||
entity_registry.async_update_entity(
|
||||
MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
name=entity_name,
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == expected_title
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
|
||||
async def test_user_flow_without_receiver(hass: HomeAssistant) -> None:
|
||||
"""Test successful setup without an infrared receiver."""
|
||||
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_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_infrared_emitter_entity")
|
||||
async def test_user_flow_stale_emitter_selection(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test user flow recovers when an emitter disappears before submit."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
replacement_emitter = "infrared.replacement_emitter"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.osram_infrared.config_flow.async_get_emitters",
|
||||
return_value=[replacement_emitter],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {
|
||||
CONF_IR_EMITTER_ENTITY_ID: "entity_not_available",
|
||||
}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_IR_EMITTER_ENTITY_ID: replacement_emitter,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"OSRAM light via {replacement_emitter}"
|
||||
assert result["data"] == {
|
||||
CONF_IR_EMITTER_ENTITY_ID: replacement_emitter,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_infrared_emitter_entity",
|
||||
"mock_infrared_receiver_entity",
|
||||
)
|
||||
async def test_user_flow_stale_receiver_selection(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test user flow recovers when a receiver disappears before submit."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
replacement_receiver = "infrared.replacement_receiver"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.osram_infrared.config_flow.async_get_receivers",
|
||||
return_value=[replacement_receiver],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
CONF_IR_RECEIVER_ENTITY_ID: MOCK_INFRARED_RECEIVER_ENTITY_ID,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {
|
||||
CONF_IR_RECEIVER_ENTITY_ID: "entity_not_available",
|
||||
}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
CONF_IR_RECEIVER_ENTITY_ID: replacement_receiver,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_IR_EMITTER_ENTITY_ID: MOCK_INFRARED_EMITTER_ENTITY_ID,
|
||||
CONF_IR_RECEIVER_ENTITY_ID: replacement_receiver,
|
||||
}
|
||||
|
||||
|
||||
async def test_user_flow_uses_entity_id_as_title_fallback(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test user flow uses the emitter entity ID when it is missing from registry."""
|
||||
emitter_entity_id = "infrared.missing_registry_emitter"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.osram_infrared.config_flow.async_get_emitters",
|
||||
return_value=[emitter_entity_id],
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.osram_infrared.config_flow.er.async_get",
|
||||
) as mock_async_get_entity_registry,
|
||||
):
|
||||
mock_async_get_entity_registry.return_value.async_get.return_value = None
|
||||
|
||||
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_IR_EMITTER_ENTITY_ID: emitter_entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"OSRAM light via {emitter_entity_id}"
|
||||
assert result["data"] == {
|
||||
CONF_IR_EMITTER_ENTITY_ID: emitter_entity_id,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Tests for the OSRAM 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
|
||||
|
||||
assert 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,495 @@
|
||||
"""Tests for the OSRAM Infrared light platform."""
|
||||
|
||||
from infrared_protocols.codes.osram.light import OSRAM_ADDRESS, OsramLightCode
|
||||
from infrared_protocols.commands.nec import NECCommand
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.infrared import InfraredReceivedSignal
|
||||
from homeassistant.components.light import (
|
||||
ATTR_EFFECT,
|
||||
ATTR_RGB_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
EFFECT_OFF,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.components.osram_infrared.const import DOMAIN
|
||||
from homeassistant.components.osram_infrared.light import CMD_REPEAT_COUNT
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.components.common import assert_availability_follows_source_entity
|
||||
from tests.components.infrared import EMITTER_ENTITY_ID, RECEIVER_ENTITY_ID
|
||||
from tests.components.infrared.common import (
|
||||
MockInfraredEmitterEntity,
|
||||
MockInfraredReceiverEntity,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return [Platform.LIGHT]
|
||||
|
||||
|
||||
@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 light entities are created with correct attributes."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry,
|
||||
mock_config_entry.entry_id,
|
||||
)
|
||||
assert len(device_entries) == 1
|
||||
|
||||
device_entry = next(iter(device_entries))
|
||||
assert device_entry.identifiers == {(DOMAIN, mock_config_entry.entry_id)}
|
||||
|
||||
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.usefixtures("init_integration")
|
||||
async def test_turn_on_sends_on_code(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test turning on the light sends the OSRAM on code."""
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.osram_light"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert (
|
||||
mock_infrared_emitter_entity.send_command_calls
|
||||
== [
|
||||
OsramLightCode.ON,
|
||||
]
|
||||
* CMD_REPEAT_COUNT
|
||||
)
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_turn_off_sends_off_code(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test turning off the light sends the OSRAM off code."""
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "light.osram_light"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert (
|
||||
mock_infrared_emitter_entity.send_command_calls
|
||||
== [
|
||||
OsramLightCode.OFF,
|
||||
]
|
||||
* CMD_REPEAT_COUNT
|
||||
)
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rgb_color", "expected_code", "expected_rgb_color"),
|
||||
[
|
||||
((180, 180, 180), OsramLightCode.WHITE, (255, 255, 255)),
|
||||
((0, 255, 0), OsramLightCode.HUE_120, (0, 255, 0)),
|
||||
((255, 0, 40), OsramLightCode.HUE_000, (255, 0, 0)),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_turn_on_with_rgb_color_sends_nearest_color_code(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
rgb_color: tuple[int, int, int],
|
||||
expected_code: OsramLightCode,
|
||||
expected_rgb_color: tuple[int, int, int],
|
||||
) -> None:
|
||||
"""Test setting RGB color sends the nearest supported OSRAM color code."""
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: "light.osram_light",
|
||||
ATTR_RGB_COLOR: rgb_color,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_infrared_emitter_entity.send_command_calls == [
|
||||
*([OsramLightCode.ON] * CMD_REPEAT_COUNT),
|
||||
*([expected_code] * CMD_REPEAT_COUNT),
|
||||
]
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
assert state
|
||||
assert tuple(state.attributes[ATTR_RGB_COLOR]) == expected_rgb_color
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("effect", "expected_code"),
|
||||
[
|
||||
("flash", OsramLightCode.FLASH),
|
||||
("strobe", OsramLightCode.STROBE),
|
||||
("smooth", OsramLightCode.SMOOTH),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_turn_on_with_effect_sends_effect_code(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
effect: str,
|
||||
expected_code: OsramLightCode,
|
||||
) -> None:
|
||||
"""Test setting an OSRAM effect sends the matching effect code."""
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: "light.osram_light",
|
||||
ATTR_EFFECT: effect,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_infrared_emitter_entity.send_command_calls == [
|
||||
*([OsramLightCode.ON] * CMD_REPEAT_COUNT),
|
||||
*([expected_code] * CMD_REPEAT_COUNT),
|
||||
]
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
assert state
|
||||
assert state.attributes[ATTR_EFFECT] == effect
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_turn_on_with_effect_off_restores_last_static_color(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test turning an effect off restores the last static color."""
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: "light.osram_light",
|
||||
ATTR_RGB_COLOR: (0, 255, 0),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: "light.osram_light",
|
||||
ATTR_EFFECT: "flash",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_infrared_emitter_entity.send_command_calls.clear()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: "light.osram_light",
|
||||
ATTR_EFFECT: EFFECT_OFF,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_infrared_emitter_entity.send_command_calls == [
|
||||
*([OsramLightCode.ON] * CMD_REPEAT_COUNT),
|
||||
*([OsramLightCode.HUE_120] * CMD_REPEAT_COUNT),
|
||||
]
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_EFFECT] == EFFECT_OFF
|
||||
assert tuple(state.attributes[ATTR_RGB_COLOR]) == (0, 255, 0)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_turn_on_with_unsupported_effect_raises(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
) -> None:
|
||||
"""Test unsupported effect is rejected before sending infrared commands."""
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match="Unsupported OSRAM infrared effect: invalid_effect",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: "light.osram_light",
|
||||
ATTR_EFFECT: "invalid_effect",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_infrared_emitter_entity.send_command_calls == []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_light_availability_follows_ir_entity(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test light becomes unavailable when IR entity is unavailable."""
|
||||
await assert_availability_follows_source_entity(
|
||||
hass,
|
||||
"light.osram_light",
|
||||
EMITTER_ENTITY_ID,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver_entity", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_off_code_updates_light_state(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test receiving the OSRAM off command updates the assumed light state."""
|
||||
command = NECCommand(
|
||||
address=OSRAM_ADDRESS,
|
||||
command=OsramLightCode.OFF,
|
||||
)
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=command.get_raw_timings())
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("received_code", "expected_rgb_color"),
|
||||
[
|
||||
(OsramLightCode.WHITE, (255, 255, 255)),
|
||||
(OsramLightCode.HUE_120, (0, 255, 0)),
|
||||
(OsramLightCode.HUE_300, (255, 0, 255)),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_receiver_entity", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_static_color_code_updates_light_state(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
received_code: OsramLightCode,
|
||||
expected_rgb_color: tuple[int, int, int],
|
||||
) -> None:
|
||||
"""Test receiving static color commands updates the assumed color state."""
|
||||
command = NECCommand(
|
||||
address=OSRAM_ADDRESS,
|
||||
command=received_code,
|
||||
)
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=command.get_raw_timings())
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
assert tuple(state.attributes[ATTR_RGB_COLOR]) == expected_rgb_color
|
||||
assert state.attributes[ATTR_EFFECT] == EFFECT_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("received_code", "expected_effect"),
|
||||
[
|
||||
(OsramLightCode.FLASH, "flash"),
|
||||
(OsramLightCode.STROBE, "strobe"),
|
||||
(OsramLightCode.SMOOTH, "smooth"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_receiver_entity", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_effect_code_updates_light_state(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
received_code: OsramLightCode,
|
||||
expected_effect: str,
|
||||
) -> None:
|
||||
"""Test receiving effect commands updates the assumed effect state."""
|
||||
command = NECCommand(
|
||||
address=OSRAM_ADDRESS,
|
||||
command=received_code,
|
||||
)
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=command.get_raw_timings())
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_EFFECT] == expected_effect
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver_entity", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_ignores_other_nec_addresses(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test receiver ignores NEC commands from other addresses."""
|
||||
command = NECCommand(
|
||||
address=0x1234,
|
||||
command=OsramLightCode.ON,
|
||||
)
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=command.get_raw_timings())
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver_entity", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_ignores_non_nec_signals(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test receiver ignores signals that cannot be decoded as NEC."""
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=[1, 2, 3, 4])
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver_entity", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_resubscribes_after_receiver_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test light resubscribes when the configured receiver becomes available again."""
|
||||
hass.states.async_set(RECEIVER_ENTITY_ID, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set(RECEIVER_ENTITY_ID, STATE_UNKNOWN)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
command = NECCommand(
|
||||
address=OSRAM_ADDRESS,
|
||||
command=OsramLightCode.ON,
|
||||
)
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=command.get_raw_timings())
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"received_code",
|
||||
[
|
||||
OsramLightCode.ON,
|
||||
OsramLightCode.MODE,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_receiver_entity", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_on_like_codes_turn_light_on(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
received_code: OsramLightCode,
|
||||
) -> None:
|
||||
"""Test receiving ON-like commands marks the assumed light state as on."""
|
||||
command = NECCommand(
|
||||
address=OSRAM_ADDRESS,
|
||||
command=received_code,
|
||||
)
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=command.get_raw_timings())
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_receiver_entity", [True])
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_receiver_ignores_unknown_osram_command(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
|
||||
) -> None:
|
||||
"""Test receiver ignores unknown OSRAM NEC commands."""
|
||||
command = NECCommand(
|
||||
address=OSRAM_ADDRESS,
|
||||
command=0xFF,
|
||||
)
|
||||
|
||||
mock_infrared_receiver_entity._handle_received_signal(
|
||||
InfraredReceivedSignal(timings=command.get_raw_timings())
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.osram_light")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
Reference in New Issue
Block a user