mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Add config flow to lg_soundbar (#71153)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
781e4571b2
commit
5fa3b90b2c
@ -63,7 +63,6 @@ SERVICE_HANDLERS = {
|
|||||||
"openhome": ServiceDetails("media_player", "openhome"),
|
"openhome": ServiceDetails("media_player", "openhome"),
|
||||||
"bose_soundtouch": ServiceDetails("media_player", "soundtouch"),
|
"bose_soundtouch": ServiceDetails("media_player", "soundtouch"),
|
||||||
"bluesound": ServiceDetails("media_player", "bluesound"),
|
"bluesound": ServiceDetails("media_player", "bluesound"),
|
||||||
"lg_smart_device": ServiceDetails("media_player", "lg_soundbar"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {}
|
OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {}
|
||||||
@ -98,6 +97,7 @@ MIGRATED_SERVICE_HANDLERS = [
|
|||||||
SERVICE_YEELIGHT,
|
SERVICE_YEELIGHT,
|
||||||
SERVICE_SABNZBD,
|
SERVICE_SABNZBD,
|
||||||
"nanoleaf_aurora",
|
"nanoleaf_aurora",
|
||||||
|
"lg_smart_device",
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_ENABLED = (
|
DEFAULT_ENABLED = (
|
||||||
|
@ -1 +1,38 @@
|
|||||||
"""The lg_soundbar component."""
|
"""The lg_soundbar component."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
|
from .config_flow import test_connect
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Set up platform from a ConfigEntry."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
# Verify the device is reachable with the given config before setting up the platform
|
||||||
|
try:
|
||||||
|
await hass.async_add_executor_job(
|
||||||
|
test_connect, entry.data[CONF_HOST], entry.data[CONF_PORT]
|
||||||
|
)
|
||||||
|
except ConnectionError as err:
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(
|
||||||
|
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
result = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
return result
|
||||||
|
78
homeassistant/components/lg_soundbar/config_flow.py
Normal file
78
homeassistant/components/lg_soundbar/config_flow.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
"""Config flow to configure the LG Soundbar integration."""
|
||||||
|
from queue import Queue
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import temescal
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
|
||||||
|
from .const import DEFAULT_PORT, DOMAIN
|
||||||
|
|
||||||
|
DATA_SCHEMA = {
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect(host, port):
|
||||||
|
"""LG Soundbar config flow test_connect."""
|
||||||
|
uuid_q = Queue(maxsize=1)
|
||||||
|
name_q = Queue(maxsize=1)
|
||||||
|
|
||||||
|
def msg_callback(response):
|
||||||
|
if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]:
|
||||||
|
uuid_q.put_nowait(response["data"]["s_uuid"])
|
||||||
|
if (
|
||||||
|
response["msg"] == "SPK_LIST_VIEW_INFO"
|
||||||
|
and "s_user_name" in response["data"]
|
||||||
|
):
|
||||||
|
name_q.put_nowait(response["data"]["s_user_name"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection = temescal.temescal(host, port=port, callback=msg_callback)
|
||||||
|
connection.get_mac_info()
|
||||||
|
connection.get_info()
|
||||||
|
details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)}
|
||||||
|
return details
|
||||||
|
except socket.timeout as err:
|
||||||
|
raise ConnectionError(f"Connection timeout with server: {host}:{port}") from err
|
||||||
|
except OSError as err:
|
||||||
|
raise ConnectionError(f"Cannot resolve hostname: {host}") from err
|
||||||
|
|
||||||
|
|
||||||
|
class LGSoundbarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""LG Soundbar config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initiated by the user."""
|
||||||
|
if user_input is None:
|
||||||
|
return self._show_form()
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
try:
|
||||||
|
details = await self.hass.async_add_executor_job(
|
||||||
|
test_connect, user_input[CONF_HOST], DEFAULT_PORT
|
||||||
|
)
|
||||||
|
except ConnectionError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(details["uuid"])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
info = {
|
||||||
|
CONF_HOST: user_input[CONF_HOST],
|
||||||
|
CONF_PORT: DEFAULT_PORT,
|
||||||
|
}
|
||||||
|
return self.async_create_entry(title=details["name"], data=info)
|
||||||
|
|
||||||
|
return self._show_form(errors)
|
||||||
|
|
||||||
|
def _show_form(self, errors=None):
|
||||||
|
"""Show the form to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(DATA_SCHEMA),
|
||||||
|
errors=errors if errors else {},
|
||||||
|
)
|
4
homeassistant/components/lg_soundbar/const.py
Normal file
4
homeassistant/components/lg_soundbar/const.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Constants for the LG Soundbar integration."""
|
||||||
|
DOMAIN = "lg_soundbar"
|
||||||
|
|
||||||
|
DEFAULT_PORT = 9741
|
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"domain": "lg_soundbar",
|
"domain": "lg_soundbar",
|
||||||
|
"config_flow": true,
|
||||||
"name": "LG Soundbars",
|
"name": "LG Soundbars",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/lg_soundbar",
|
"documentation": "https://www.home-assistant.io/integrations/lg_soundbar",
|
||||||
"requirements": ["temescal==0.3"],
|
"requirements": ["temescal==0.5"],
|
||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["temescal"]
|
"loggers": ["temescal"]
|
||||||
|
@ -7,26 +7,33 @@ from homeassistant.components.media_player import (
|
|||||||
MediaPlayerEntity,
|
MediaPlayerEntity,
|
||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.const import STATE_ON
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config_entry: ConfigEntry,
|
||||||
add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the LG platform."""
|
"""Set up media_player from a config entry created in the integrations UI."""
|
||||||
if discovery_info is not None:
|
async_add_entities(
|
||||||
add_entities([LGDevice(discovery_info)])
|
[
|
||||||
|
LGDevice(
|
||||||
|
config_entry.data[CONF_HOST],
|
||||||
|
config_entry.data[CONF_PORT],
|
||||||
|
config_entry.unique_id,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LGDevice(MediaPlayerEntity):
|
class LGDevice(MediaPlayerEntity):
|
||||||
"""Representation of an LG soundbar device."""
|
"""Representation of an LG soundbar device."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
_attr_supported_features = (
|
_attr_supported_features = (
|
||||||
MediaPlayerEntityFeature.VOLUME_SET
|
MediaPlayerEntityFeature.VOLUME_SET
|
||||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||||
@ -34,13 +41,13 @@ class LGDevice(MediaPlayerEntity):
|
|||||||
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, discovery_info):
|
def __init__(self, host, port, unique_id):
|
||||||
"""Initialize the LG speakers."""
|
"""Initialize the LG speakers."""
|
||||||
self._host = discovery_info["host"]
|
self._host = host
|
||||||
self._port = discovery_info["port"]
|
self._port = port
|
||||||
self._hostname = discovery_info["hostname"]
|
self._attr_unique_id = unique_id
|
||||||
|
|
||||||
self._name = self._hostname.split(".")[0]
|
self._name = None
|
||||||
self._volume = 0
|
self._volume = 0
|
||||||
self._volume_min = 0
|
self._volume_min = 0
|
||||||
self._volume_max = 0
|
self._volume_max = 0
|
||||||
@ -68,6 +75,8 @@ class LGDevice(MediaPlayerEntity):
|
|||||||
self._device = temescal.temescal(
|
self._device = temescal.temescal(
|
||||||
self._host, port=self._port, callback=self.handle_event
|
self._host, port=self._port, callback=self.handle_event
|
||||||
)
|
)
|
||||||
|
self._device.get_product_info()
|
||||||
|
self._device.get_mac_info()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def handle_event(self, response):
|
def handle_event(self, response):
|
||||||
@ -116,7 +125,8 @@ class LGDevice(MediaPlayerEntity):
|
|||||||
if "i_curr_eq" in data:
|
if "i_curr_eq" in data:
|
||||||
self._equaliser = data["i_curr_eq"]
|
self._equaliser = data["i_curr_eq"]
|
||||||
if "s_user_name" in data:
|
if "s_user_name" in data:
|
||||||
self._name = data["s_user_name"]
|
self._attr_name = data["s_user_name"]
|
||||||
|
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
@ -125,17 +135,6 @@ class LGDevice(MediaPlayerEntity):
|
|||||||
self._device.get_info()
|
self._device.get_info()
|
||||||
self._device.get_func()
|
self._device.get_func()
|
||||||
self._device.get_settings()
|
self._device.get_settings()
|
||||||
self._device.get_product_info()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""No polling needed."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
"""Return the name of the device."""
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
|
18
homeassistant/components/lg_soundbar/strings.json
Normal file
18
homeassistant/components/lg_soundbar/strings.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"existing_instance_updated": "Updated existing configuration.",
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
homeassistant/components/lg_soundbar/translations/en.json
Normal file
18
homeassistant/components/lg_soundbar/translations/en.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Service is already configured",
|
||||||
|
"existing_instance_updated": "Updated existing configuration."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -190,6 +190,7 @@ FLOWS = {
|
|||||||
"kulersky",
|
"kulersky",
|
||||||
"launch_library",
|
"launch_library",
|
||||||
"laundrify",
|
"laundrify",
|
||||||
|
"lg_soundbar",
|
||||||
"life360",
|
"life360",
|
||||||
"lifx",
|
"lifx",
|
||||||
"litejet",
|
"litejet",
|
||||||
|
@ -2289,7 +2289,7 @@ tellcore-py==1.1.2
|
|||||||
tellduslive==0.10.11
|
tellduslive==0.10.11
|
||||||
|
|
||||||
# homeassistant.components.lg_soundbar
|
# homeassistant.components.lg_soundbar
|
||||||
temescal==0.3
|
temescal==0.5
|
||||||
|
|
||||||
# homeassistant.components.temper
|
# homeassistant.components.temper
|
||||||
temperusb==1.5.3
|
temperusb==1.5.3
|
||||||
|
@ -1524,6 +1524,9 @@ tailscale==0.2.0
|
|||||||
# homeassistant.components.tellduslive
|
# homeassistant.components.tellduslive
|
||||||
tellduslive==0.10.11
|
tellduslive==0.10.11
|
||||||
|
|
||||||
|
# homeassistant.components.lg_soundbar
|
||||||
|
temescal==0.5
|
||||||
|
|
||||||
# homeassistant.components.powerwall
|
# homeassistant.components.powerwall
|
||||||
tesla-powerwall==0.3.18
|
tesla-powerwall==0.3.18
|
||||||
|
|
||||||
|
1
tests/components/lg_soundbar/__init__.py
Normal file
1
tests/components/lg_soundbar/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the lg_soundbar component."""
|
95
tests/components/lg_soundbar/test_config_flow.py
Normal file
95
tests/components/lg_soundbar/test_config_flow.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""Test the lg_soundbar config flow."""
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass):
|
||||||
|
"""Test we get the form."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.lg_soundbar.config_flow.temescal",
|
||||||
|
return_value=MagicMock(),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.lg_soundbar.config_flow.test_connect",
|
||||||
|
return_value={"uuid": "uuid", "name": "name"},
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.lg_soundbar.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == "name"
|
||||||
|
assert result2["data"] == {
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PORT: DEFAULT_PORT,
|
||||||
|
}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.lg_soundbar.config_flow.test_connect",
|
||||||
|
side_effect=ConnectionError,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_already_configured(hass):
|
||||||
|
"""Test we handle already configured error."""
|
||||||
|
mock_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PORT: 0000,
|
||||||
|
},
|
||||||
|
unique_id="uuid",
|
||||||
|
)
|
||||||
|
mock_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.lg_soundbar.config_flow.test_connect",
|
||||||
|
return_value={"uuid": "uuid", "name": "name"},
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "abort"
|
||||||
|
assert result2["reason"] == "already_configured"
|
Loading…
x
Reference in New Issue
Block a user