mirror of
https://github.com/home-assistant/core.git
synced 2026-03-17 16:32:04 +01:00
Compare commits
21 Commits
windows-98
...
lg_infrare
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b6ba56edd | ||
|
|
9e132c4c69 | ||
|
|
b2ec8f3ae5 | ||
|
|
bb48ba36f5 | ||
|
|
dc4848f035 | ||
|
|
19448eba98 | ||
|
|
e7f9f65d12 | ||
|
|
a59286e51b | ||
|
|
af331c7658 | ||
|
|
76d21ce633 | ||
|
|
a5a72ee0bd | ||
|
|
ea8f248ddf | ||
|
|
4bd30b600f | ||
|
|
9f485084da | ||
|
|
63db1cc080 | ||
|
|
91fab62af6 | ||
|
|
e6f45bd9f6 | ||
|
|
81415a3cb1 | ||
|
|
ed1bb685da | ||
|
|
6c610dfe73 | ||
|
|
90bacbb98e |
@@ -325,6 +325,7 @@ homeassistant.components.ld2410_ble.*
|
||||
homeassistant.components.led_ble.*
|
||||
homeassistant.components.lektrico.*
|
||||
homeassistant.components.letpot.*
|
||||
homeassistant.components.lg_infrared.*
|
||||
homeassistant.components.libre_hardware_monitor.*
|
||||
homeassistant.components.lidarr.*
|
||||
homeassistant.components.lifx.*
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"domain": "lg",
|
||||
"name": "LG",
|
||||
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
||||
"integrations": [
|
||||
"lg_infrared",
|
||||
"lg_netcast",
|
||||
"lg_soundbar",
|
||||
"lg_thinq",
|
||||
"webostv"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/infrared",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["infrared-protocols==1.0.0"]
|
||||
"requirements": ["infrared-protocols==1.1.0"]
|
||||
}
|
||||
|
||||
20
homeassistant/components/lg_infrared/__init__.py
Normal file
20
homeassistant/components/lg_infrared/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""LG IR Remote integration for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up LG IR from a config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a LG IR config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
82
homeassistant/components/lg_infrared/config_flow.py
Normal file
82
homeassistant/components/lg_infrared/config_flow.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Config flow for LG IR integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.infrared import (
|
||||
DOMAIN as INFRARED_DOMAIN,
|
||||
async_get_emitters,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers.selector import (
|
||||
EntitySelector,
|
||||
EntitySelectorConfig,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, DOMAIN, LGDeviceType
|
||||
|
||||
DEVICE_TYPE_NAMES: dict[LGDeviceType, str] = {
|
||||
LGDeviceType.TV: "TV",
|
||||
}
|
||||
|
||||
|
||||
class LgIrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle config flow for LG IR."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
entities = async_get_emitters(self.hass)
|
||||
if not entities:
|
||||
return self.async_abort(reason="no_emitters")
|
||||
|
||||
valid_entity_ids = [entity.entity_id for entity in entities]
|
||||
|
||||
if user_input is not None:
|
||||
entity_id = user_input[CONF_INFRARED_ENTITY_ID]
|
||||
device_type = user_input[CONF_DEVICE_TYPE]
|
||||
|
||||
await self.async_set_unique_id(f"lg_ir_{device_type}_{entity_id}")
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Get entity name for the title
|
||||
entity_name = next(
|
||||
(
|
||||
entity.name or entity.entity_id
|
||||
for entity in entities
|
||||
if entity.entity_id == entity_id
|
||||
),
|
||||
entity_id,
|
||||
)
|
||||
device_type_name = DEVICE_TYPE_NAMES[LGDeviceType(device_type)]
|
||||
title = f"LG {device_type_name} via {entity_name}"
|
||||
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DEVICE_TYPE): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_type.value for device_type in LGDeviceType],
|
||||
translation_key=CONF_DEVICE_TYPE,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_INFRARED_ENTITY_ID): EntitySelector(
|
||||
EntitySelectorConfig(
|
||||
domain=INFRARED_DOMAIN,
|
||||
include_entities=valid_entity_ids,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
13
homeassistant/components/lg_infrared/const.py
Normal file
13
homeassistant/components/lg_infrared/const.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Constants for the LG IR integration."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
DOMAIN = "lg_infrared"
|
||||
CONF_INFRARED_ENTITY_ID = "infrared_entity_id"
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
|
||||
|
||||
class LGDeviceType(StrEnum):
|
||||
"""LG device types."""
|
||||
|
||||
TV = "tv"
|
||||
11
homeassistant/components/lg_infrared/manifest.json
Normal file
11
homeassistant/components/lg_infrared/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "lg_infrared",
|
||||
"name": "LG Infrared",
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"dependencies": ["infrared"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/lg_infrared",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze"
|
||||
}
|
||||
137
homeassistant/components/lg_infrared/media_player.py
Normal file
137
homeassistant/components/lg_infrared/media_player.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Media player platform for LG IR integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from infrared_protocols.codes.lg.tv import LGTVCode, make_command as make_lg_tv_command
|
||||
|
||||
from homeassistant.components.infrared import async_send_command
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import 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_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, DOMAIN, LGDeviceType
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up LG IR media player from config entry."""
|
||||
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
|
||||
device_type = entry.data.get(CONF_DEVICE_TYPE, LGDeviceType.TV)
|
||||
if device_type == LGDeviceType.TV:
|
||||
async_add_entities([LgIrTvMediaPlayer(entry, infrared_entity_id)])
|
||||
|
||||
|
||||
class LgIrTvMediaPlayer(MediaPlayerEntity):
|
||||
"""LG IR media player entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_assumed_state = True
|
||||
_attr_device_class = MediaPlayerDeviceClass.TV
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
)
|
||||
|
||||
def __init__(self, entry: ConfigEntry, infrared_entity_id: str) -> None:
|
||||
"""Initialize LG IR media player."""
|
||||
self._entry = entry
|
||||
self._infrared_entity_id = infrared_entity_id
|
||||
self._attr_unique_id = f"{entry.entry_id}_media_player"
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.entry_id)}, name="LG TV", manufacturer="LG"
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to infrared entity state changes."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def _async_ir_state_changed(event: Event[EventStateChangedData]) -> None:
|
||||
"""Handle infrared entity state changes."""
|
||||
new_state = event.data["new_state"]
|
||||
self._attr_available = (
|
||||
new_state is not None and new_state.state != STATE_UNAVAILABLE
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [self._infrared_entity_id], _async_ir_state_changed
|
||||
)
|
||||
)
|
||||
|
||||
# Set initial availability based on current infrared entity state
|
||||
ir_state = self.hass.states.get(self._infrared_entity_id)
|
||||
self._attr_available = (
|
||||
ir_state is not None and ir_state.state != STATE_UNAVAILABLE
|
||||
)
|
||||
|
||||
async def _send_command(self, code: LGTVCode) -> None:
|
||||
"""Send an IR command using the LG protocol."""
|
||||
await async_send_command(
|
||||
self.hass,
|
||||
self._infrared_entity_id,
|
||||
make_lg_tv_command(code),
|
||||
context=self._context,
|
||||
)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on the TV."""
|
||||
await self._send_command(LGTVCode.POWER)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off the TV."""
|
||||
await self._send_command(LGTVCode.POWER)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Send volume up command."""
|
||||
await self._send_command(LGTVCode.VOLUME_UP)
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Send volume down command."""
|
||||
await self._send_command(LGTVCode.VOLUME_DOWN)
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Send mute command."""
|
||||
await self._send_command(LGTVCode.MUTE)
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send channel up command."""
|
||||
await self._send_command(LGTVCode.CHANNEL_UP)
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send channel down command."""
|
||||
await self._send_command(LGTVCode.CHANNEL_DOWN)
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
await self._send_command(LGTVCode.PLAY)
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
await self._send_command(LGTVCode.PAUSE)
|
||||
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
await self._send_command(LGTVCode.STOP)
|
||||
127
homeassistant/components/lg_infrared/quality_scale.yaml
Normal file
127
homeassistant/components/lg_infrared/quality_scale.yaml
Normal file
@@ -0,0 +1,127 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not poll.
|
||||
brands:
|
||||
status: exempt
|
||||
comment: |
|
||||
This is a proof of concept integration, brand assets will be added later.
|
||||
common-modules:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration is simple and does not share patterns with others.
|
||||
config-flow-test-coverage:
|
||||
status: exempt
|
||||
comment: |
|
||||
This is a proof of concept integration, config flow tests will be added later.
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description:
|
||||
status: exempt
|
||||
comment: |
|
||||
This is a proof of concept integration, documentation will be added later.
|
||||
docs-installation-instructions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This is a proof of concept integration, documentation will be added later.
|
||||
docs-removal-instructions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This is a proof of concept integration, documentation will be added later.
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not subscribe to events.
|
||||
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: |
|
||||
Setup validation is handled by checking emitter existence in remote.py.
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: todo
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not require authentication.
|
||||
test-coverage: todo
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not support discovery.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration is configured manually via config flow.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Each config entry creates a single device.
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: |
|
||||
The remote entity is the primary entity and does not need a category.
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: |
|
||||
Remote entities do not have a device class.
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: |
|
||||
The remote entity is the primary entity and should be enabled by default.
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not have repairable issues.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Each config entry manages exactly one device.
|
||||
|
||||
# Platinum
|
||||
async-dependency:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration only depends on ir_proxy which is part of Home Assistant.
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not make HTTP requests.
|
||||
strict-typing: todo
|
||||
118
homeassistant/components/lg_infrared/strings.json
Normal file
118
homeassistant/components/lg_infrared/strings.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This LG device has already been configured with this transmitter.",
|
||||
"no_emitters": "No infrared transmitter entities found. Please set up an infrared device first."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"device_type": "Device type",
|
||||
"infrared_entity_id": "Infrared transmitter"
|
||||
},
|
||||
"data_description": {
|
||||
"device_type": "The type of LG device to control.",
|
||||
"infrared_entity_id": "The infrared transmitter entity to use for sending commands."
|
||||
},
|
||||
"description": "Select the device type and the infrared transmitter entity to use for controlling your LG device.",
|
||||
"title": "Set up LG IR Remote"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"back": {
|
||||
"name": "Back"
|
||||
},
|
||||
"down": {
|
||||
"name": "Down"
|
||||
},
|
||||
"exit": {
|
||||
"name": "Exit"
|
||||
},
|
||||
"guide": {
|
||||
"name": "Guide"
|
||||
},
|
||||
"hdmi_1": {
|
||||
"name": "HDMI 1"
|
||||
},
|
||||
"hdmi_2": {
|
||||
"name": "HDMI 2"
|
||||
},
|
||||
"hdmi_3": {
|
||||
"name": "HDMI 3"
|
||||
},
|
||||
"hdmi_4": {
|
||||
"name": "HDMI 4"
|
||||
},
|
||||
"home": {
|
||||
"name": "Home"
|
||||
},
|
||||
"info": {
|
||||
"name": "Info"
|
||||
},
|
||||
"input": {
|
||||
"name": "Input"
|
||||
},
|
||||
"left": {
|
||||
"name": "Left"
|
||||
},
|
||||
"menu": {
|
||||
"name": "Menu"
|
||||
},
|
||||
"num_0": {
|
||||
"name": "0"
|
||||
},
|
||||
"num_1": {
|
||||
"name": "1"
|
||||
},
|
||||
"num_2": {
|
||||
"name": "2"
|
||||
},
|
||||
"num_3": {
|
||||
"name": "3"
|
||||
},
|
||||
"num_4": {
|
||||
"name": "4"
|
||||
},
|
||||
"num_5": {
|
||||
"name": "5"
|
||||
},
|
||||
"num_6": {
|
||||
"name": "6"
|
||||
},
|
||||
"num_7": {
|
||||
"name": "7"
|
||||
},
|
||||
"num_8": {
|
||||
"name": "8"
|
||||
},
|
||||
"num_9": {
|
||||
"name": "9"
|
||||
},
|
||||
"ok": {
|
||||
"name": "OK"
|
||||
},
|
||||
"power_off": {
|
||||
"name": "Power off"
|
||||
},
|
||||
"power_on": {
|
||||
"name": "Power on"
|
||||
},
|
||||
"right": {
|
||||
"name": "Right"
|
||||
},
|
||||
"up": {
|
||||
"name": "Up"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_type": {
|
||||
"options": {
|
||||
"hifi": "Hi-Fi",
|
||||
"tv": "TV"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -381,6 +381,7 @@ FLOWS = {
|
||||
"led_ble",
|
||||
"lektrico",
|
||||
"letpot",
|
||||
"lg_infrared",
|
||||
"lg_netcast",
|
||||
"lg_soundbar",
|
||||
"lg_thinq",
|
||||
|
||||
@@ -3633,6 +3633,12 @@
|
||||
"lg": {
|
||||
"name": "LG",
|
||||
"integrations": {
|
||||
"lg_infrared": {
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "LG Infrared"
|
||||
},
|
||||
"lg_netcast": {
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
|
||||
10
mypy.ini
generated
10
mypy.ini
generated
@@ -3006,6 +3006,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.lg_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.libre_hardware_monitor.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -31,7 +31,7 @@ home-assistant-bluetooth==1.13.1
|
||||
home-assistant-intents==2026.3.3
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
infrared-protocols==1.0.0
|
||||
infrared-protocols==1.1.0
|
||||
Jinja2==3.1.6
|
||||
lru-dict==1.3.0
|
||||
mutagen==1.47.0
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -1313,7 +1313,7 @@ influxdb-client==1.50.0
|
||||
influxdb==5.3.1
|
||||
|
||||
# homeassistant.components.infrared
|
||||
infrared-protocols==1.0.0
|
||||
infrared-protocols==1.1.0
|
||||
|
||||
# homeassistant.components.inkbird
|
||||
inkbird-ble==1.1.1
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -1162,7 +1162,7 @@ influxdb-client==1.50.0
|
||||
influxdb==5.3.1
|
||||
|
||||
# homeassistant.components.infrared
|
||||
infrared-protocols==1.0.0
|
||||
infrared-protocols==1.1.0
|
||||
|
||||
# homeassistant.components.inkbird
|
||||
inkbird-ble==1.1.1
|
||||
|
||||
1
tests/components/lg_infrared/__init__.py
Normal file
1
tests/components/lg_infrared/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the LG Infrared integration."""
|
||||
109
tests/components/lg_infrared/conftest.py
Normal file
109
tests/components/lg_infrared/conftest.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Common fixtures for the LG Infrared tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
from infrared_protocols import Command as InfraredCommand
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.infrared import (
|
||||
DATA_COMPONENT as INFRARED_DATA_COMPONENT,
|
||||
DOMAIN as INFRARED_DOMAIN,
|
||||
InfraredEntity,
|
||||
)
|
||||
from homeassistant.components.lg_infrared.const import (
|
||||
CONF_DEVICE_TYPE,
|
||||
CONF_INFRARED_ENTITY_ID,
|
||||
DOMAIN,
|
||||
LGDeviceType,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_INFRARED_ENTITY_ID = "infrared.test_ir_transmitter"
|
||||
|
||||
|
||||
class MockInfraredEntity(InfraredEntity):
|
||||
"""Mock infrared entity for testing."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Test IR transmitter"
|
||||
|
||||
def __init__(self, unique_id: str) -> None:
|
||||
"""Initialize mock entity."""
|
||||
self._attr_unique_id = unique_id
|
||||
self.send_command_calls: list[InfraredCommand] = []
|
||||
|
||||
async def async_send_command(self, command: InfraredCommand) -> None:
|
||||
"""Mock send command."""
|
||||
self.send_command_calls.append(command)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return a mock config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
entry_id="01JTEST0000000000000000000",
|
||||
title="LG TV via Test IR transmitter",
|
||||
data={
|
||||
CONF_DEVICE_TYPE: LGDeviceType.TV,
|
||||
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_ENTITY_ID,
|
||||
},
|
||||
unique_id=f"lg_ir_tv_{MOCK_INFRARED_ENTITY_ID}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_infrared_entity() -> MockInfraredEntity:
|
||||
"""Return a mock infrared entity."""
|
||||
return MockInfraredEntity("test_ir_transmitter")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_make_lg_tv_command() -> Generator[None]:
|
||||
"""Patch make_command to return the LGTVCode directly.
|
||||
|
||||
This allows tests to assert on the high-level code enum value
|
||||
rather than the raw NEC timings.
|
||||
"""
|
||||
with patch(
|
||||
"homeassistant.components.lg_infrared.media_player.make_lg_tv_command",
|
||||
side_effect=lambda code, **kwargs: code,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_infrared_entity: MockInfraredEntity,
|
||||
mock_make_lg_tv_command: None,
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the LG Infrared integration for testing."""
|
||||
assert await async_setup_component(hass, INFRARED_DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
infrared_component = hass.data[INFRARED_DATA_COMPONENT]
|
||||
await infrared_component.async_add_entities([mock_infrared_entity])
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.lg_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,54 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[media_player.lg_tv-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.lg_tv',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <MediaPlayerDeviceClass.TV: 'tv'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lg_infrared',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 21945>,
|
||||
'translation_key': None,
|
||||
'unique_id': '01JTEST0000000000000000000_media_player',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[media_player.lg_tv-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'assumed_state': True,
|
||||
'device_class': 'tv',
|
||||
'friendly_name': 'LG TV',
|
||||
'supported_features': <MediaPlayerEntityFeature: 21945>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.lg_tv',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
134
tests/components/lg_infrared/test_config_flow.py
Normal file
134
tests/components/lg_infrared/test_config_flow.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Tests for the LG Infrared config flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.infrared import (
|
||||
DATA_COMPONENT as INFRARED_DATA_COMPONENT,
|
||||
DOMAIN as INFRARED_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.lg_infrared.const import (
|
||||
CONF_DEVICE_TYPE,
|
||||
CONF_INFRARED_ENTITY_ID,
|
||||
DOMAIN,
|
||||
LGDeviceType,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import MOCK_INFRARED_ENTITY_ID, MockInfraredEntity
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_infrared(
|
||||
hass: HomeAssistant, mock_infrared_entity: MockInfraredEntity
|
||||
) -> None:
|
||||
"""Set up the infrared component with a mock entity."""
|
||||
assert await async_setup_component(hass, INFRARED_DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
component = hass.data[INFRARED_DATA_COMPONENT]
|
||||
await component.async_add_entities([mock_infrared_entity])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_infrared")
|
||||
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_DEVICE_TYPE: LGDeviceType.TV,
|
||||
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_ENTITY_ID,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "LG TV via Test IR transmitter"
|
||||
assert result["data"] == {
|
||||
CONF_DEVICE_TYPE: LGDeviceType.TV,
|
||||
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_ENTITY_ID,
|
||||
}
|
||||
assert result["result"].unique_id == f"lg_ir_tv_{MOCK_INFRARED_ENTITY_ID}"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_infrared")
|
||||
async def test_user_flow_already_configured(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test user flow aborts when entry 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_DEVICE_TYPE: LGDeviceType.TV,
|
||||
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_ENTITY_ID,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_user_flow_no_emitters(hass: HomeAssistant) -> None:
|
||||
"""Test user flow aborts when no infrared emitters exist."""
|
||||
assert await async_setup_component(hass, INFRARED_DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_emitters"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("setup_infrared")
|
||||
@pytest.mark.parametrize(
|
||||
("entity_name", "expected_title"),
|
||||
[
|
||||
("Test IR transmitter", "LG TV via Test IR transmitter"),
|
||||
("AC IR emitter", "LG TV via AC IR emitter"),
|
||||
],
|
||||
)
|
||||
async def test_user_flow_title_from_entity_name(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_entity: MockInfraredEntity,
|
||||
entity_name: str,
|
||||
expected_title: str,
|
||||
) -> None:
|
||||
"""Test config entry title uses the entity name."""
|
||||
mock_infrared_entity.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_DEVICE_TYPE: LGDeviceType.TV,
|
||||
CONF_INFRARED_ENTITY_ID: MOCK_INFRARED_ENTITY_ID,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == expected_title
|
||||
21
tests/components/lg_infrared/test_init.py
Normal file
21
tests/components/lg_infrared/test_init.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Tests for the LG Infrared integration setup."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_and_unload_entry(
|
||||
hass: HomeAssistant, init_integration: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test setting up and unloading a config entry."""
|
||||
entry = init_integration
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
121
tests/components/lg_infrared/test_media_player.py
Normal file
121
tests/components/lg_infrared/test_media_player.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Tests for the LG Infrared media player platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from infrared_protocols.codes.lg.tv import LGTVCode
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_MEDIA_NEXT_TRACK,
|
||||
SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
SERVICE_MEDIA_STOP,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
SERVICE_VOLUME_DOWN,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_UP,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .conftest import MOCK_INFRARED_ENTITY_ID, MockInfraredEntity
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
MEDIA_PLAYER_ENTITY_ID = "media_player.lg_tv"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the media player entity is created with correct attributes."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
# Verify entity belongs to the correct device
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={("lg_infrared", mock_config_entry.entry_id)}
|
||||
)
|
||||
assert device_entry
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
for entity_entry in entity_entries:
|
||||
assert entity_entry.device_id == device_entry.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data", "expected_code"),
|
||||
[
|
||||
(SERVICE_TURN_ON, {}, LGTVCode.POWER),
|
||||
(SERVICE_TURN_OFF, {}, LGTVCode.POWER),
|
||||
(SERVICE_VOLUME_UP, {}, LGTVCode.VOLUME_UP),
|
||||
(SERVICE_VOLUME_DOWN, {}, LGTVCode.VOLUME_DOWN),
|
||||
(SERVICE_VOLUME_MUTE, {"is_volume_muted": True}, LGTVCode.MUTE),
|
||||
(SERVICE_MEDIA_NEXT_TRACK, {}, LGTVCode.CHANNEL_UP),
|
||||
(SERVICE_MEDIA_PREVIOUS_TRACK, {}, LGTVCode.CHANNEL_DOWN),
|
||||
(SERVICE_MEDIA_PLAY, {}, LGTVCode.PLAY),
|
||||
(SERVICE_MEDIA_PAUSE, {}, LGTVCode.PAUSE),
|
||||
(SERVICE_MEDIA_STOP, {}, LGTVCode.STOP),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_media_player_action_sends_correct_code(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_entity: MockInfraredEntity,
|
||||
service: str,
|
||||
service_data: dict[str, bool],
|
||||
expected_code: LGTVCode,
|
||||
) -> None:
|
||||
"""Test each media player action sends the correct IR code."""
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: MEDIA_PLAYER_ENTITY_ID, **service_data},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_entity.send_command_calls) == 1
|
||||
assert mock_infrared_entity.send_command_calls[0] == expected_code
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_media_player_availability_follows_ir_entity(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test media player becomes unavailable when IR entity is unavailable."""
|
||||
# Initially available
|
||||
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# Make IR entity unavailable
|
||||
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Restore IR entity
|
||||
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, "2026-01-01T00:00:00.000")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
Reference in New Issue
Block a user