mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add Linkplay mTLS/HTTPS and improve logging (#124307)
* Work * Implement 0.0.8 changes, fixup tests * Cleanup * Implement new playmodes, close clientsession upon ha close * Implement new playmodes, close clientsession upon ha close * Add test for zeroconf bridge failure * Bump 0.0.9 Address old comments in 113940 * Exact _async_register_default_clientsession_shutdown
This commit is contained in:
parent
f34b449f61
commit
b9db9eeab2
@ -1,17 +1,22 @@
|
|||||||
"""Support for LinkPlay devices."""
|
"""Support for LinkPlay devices."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
from linkplay.bridge import LinkPlayBridge
|
from linkplay.bridge import LinkPlayBridge
|
||||||
from linkplay.discovery import linkplay_factory_bridge
|
from linkplay.discovery import linkplay_factory_httpapi_bridge
|
||||||
|
from linkplay.exceptions import LinkPlayRequestException
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import PLATFORMS
|
from .const import PLATFORMS
|
||||||
|
from .utils import async_get_client_session
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class LinkPlayData:
|
class LinkPlayData:
|
||||||
"""Data for LinkPlay."""
|
"""Data for LinkPlay."""
|
||||||
|
|
||||||
@ -24,16 +29,17 @@ type LinkPlayConfigEntry = ConfigEntry[LinkPlayData]
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> bool:
|
||||||
"""Async setup hass config entry. Called when an entry has been setup."""
|
"""Async setup hass config entry. Called when an entry has been setup."""
|
||||||
|
|
||||||
session = async_get_clientsession(hass)
|
session: ClientSession = await async_get_client_session(hass)
|
||||||
if (
|
bridge: LinkPlayBridge | None = None
|
||||||
bridge := await linkplay_factory_bridge(entry.data[CONF_HOST], session)
|
|
||||||
) is None:
|
try:
|
||||||
|
bridge = await linkplay_factory_httpapi_bridge(entry.data[CONF_HOST], session)
|
||||||
|
except LinkPlayRequestException as exception:
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
f"Failed to connect to LinkPlay device at {entry.data[CONF_HOST]}"
|
f"Failed to connect to LinkPlay device at {entry.data[CONF_HOST]}"
|
||||||
)
|
) from exception
|
||||||
|
|
||||||
entry.runtime_data = LinkPlayData()
|
entry.runtime_data = LinkPlayData(bridge=bridge)
|
||||||
entry.runtime_data.bridge = bridge
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
"""Config flow to configure LinkPlay component."""
|
"""Config flow to configure LinkPlay component."""
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from linkplay.discovery import linkplay_factory_bridge
|
from aiohttp import ClientSession
|
||||||
|
from linkplay.bridge import LinkPlayBridge
|
||||||
|
from linkplay.discovery import linkplay_factory_httpapi_bridge
|
||||||
|
from linkplay.exceptions import LinkPlayRequestException
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .utils import async_get_client_session
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
@ -25,10 +31,15 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle Zeroconf discovery."""
|
"""Handle Zeroconf discovery."""
|
||||||
|
|
||||||
session = async_get_clientsession(self.hass)
|
session: ClientSession = await async_get_client_session(self.hass)
|
||||||
bridge = await linkplay_factory_bridge(discovery_info.host, session)
|
bridge: LinkPlayBridge | None = None
|
||||||
|
|
||||||
if bridge is None:
|
try:
|
||||||
|
bridge = await linkplay_factory_httpapi_bridge(discovery_info.host, session)
|
||||||
|
except LinkPlayRequestException:
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Failed to connect to LinkPlay device at %s", discovery_info.host
|
||||||
|
)
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
|
||||||
self.data[CONF_HOST] = discovery_info.host
|
self.data[CONF_HOST] = discovery_info.host
|
||||||
@ -66,14 +77,26 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a flow initialized by the user."""
|
"""Handle a flow initialized by the user."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input:
|
if user_input:
|
||||||
session = async_get_clientsession(self.hass)
|
session: ClientSession = await async_get_client_session(self.hass)
|
||||||
bridge = await linkplay_factory_bridge(user_input[CONF_HOST], session)
|
bridge: LinkPlayBridge | None = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
bridge = await linkplay_factory_httpapi_bridge(
|
||||||
|
user_input[CONF_HOST], session
|
||||||
|
)
|
||||||
|
except LinkPlayRequestException:
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Failed to connect to LinkPlay device at %s", user_input[CONF_HOST]
|
||||||
|
)
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
|
||||||
if bridge is not None:
|
if bridge is not None:
|
||||||
self.data[CONF_HOST] = user_input[CONF_HOST]
|
self.data[CONF_HOST] = user_input[CONF_HOST]
|
||||||
self.data[CONF_MODEL] = bridge.device.name
|
self.data[CONF_MODEL] = bridge.device.name
|
||||||
|
|
||||||
await self.async_set_unique_id(bridge.device.uuid)
|
await self.async_set_unique_id(
|
||||||
|
bridge.device.uuid, raise_on_progress=False
|
||||||
|
)
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_if_unique_id_configured(
|
||||||
updates={CONF_HOST: self.data[CONF_HOST]}
|
updates={CONF_HOST: self.data[CONF_HOST]}
|
||||||
)
|
)
|
||||||
@ -83,7 +106,6 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data={CONF_HOST: self.data[CONF_HOST]},
|
data={CONF_HOST: self.data[CONF_HOST]},
|
||||||
)
|
)
|
||||||
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
|
@ -4,3 +4,4 @@ from homeassistant.const import Platform
|
|||||||
|
|
||||||
DOMAIN = "linkplay"
|
DOMAIN = "linkplay"
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||||
|
CONF_SESSION = "session"
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/linkplay",
|
"documentation": "https://www.home-assistant.io/integrations/linkplay",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["python-linkplay==0.0.8"],
|
"requirements": ["python-linkplay==0.0.9"],
|
||||||
"zeroconf": ["_linkplay._tcp.local."]
|
"zeroconf": ["_linkplay._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,17 @@ SOURCE_MAP: dict[PlayingMode, str] = {
|
|||||||
PlayingMode.XLR: "XLR",
|
PlayingMode.XLR: "XLR",
|
||||||
PlayingMode.HDMI: "HDMI",
|
PlayingMode.HDMI: "HDMI",
|
||||||
PlayingMode.OPTICAL_2: "Optical 2",
|
PlayingMode.OPTICAL_2: "Optical 2",
|
||||||
|
PlayingMode.EXTERN_BLUETOOTH: "External Bluetooth",
|
||||||
|
PlayingMode.PHONO: "Phono",
|
||||||
|
PlayingMode.ARC: "ARC",
|
||||||
|
PlayingMode.COAXIAL_2: "Coaxial 2",
|
||||||
|
PlayingMode.TF_CARD_1: "SD Card 1",
|
||||||
|
PlayingMode.TF_CARD_2: "SD Card 2",
|
||||||
|
PlayingMode.CD: "CD",
|
||||||
|
PlayingMode.DAB: "DAB Radio",
|
||||||
|
PlayingMode.FM: "FM Radio",
|
||||||
|
PlayingMode.RCA: "RCA",
|
||||||
|
PlayingMode.UDISK: "USB",
|
||||||
}
|
}
|
||||||
|
|
||||||
SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()}
|
SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()}
|
||||||
|
@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from linkplay.utils import async_create_unverified_client_session
|
||||||
|
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||||
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import CONF_SESSION, DOMAIN
|
||||||
|
|
||||||
MANUFACTURER_ARTSOUND: Final[str] = "ArtSound"
|
MANUFACTURER_ARTSOUND: Final[str] = "ArtSound"
|
||||||
MANUFACTURER_ARYLIC: Final[str] = "Arylic"
|
MANUFACTURER_ARYLIC: Final[str] = "Arylic"
|
||||||
MANUFACTURER_IEAST: Final[str] = "iEAST"
|
MANUFACTURER_IEAST: Final[str] = "iEAST"
|
||||||
@ -44,3 +52,22 @@ def get_info_from_project(project: str) -> tuple[str, str]:
|
|||||||
return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5
|
return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5
|
||||||
case _:
|
case _:
|
||||||
return MANUFACTURER_GENERIC, MODELS_GENERIC
|
return MANUFACTURER_GENERIC, MODELS_GENERIC
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_client_session(hass: HomeAssistant) -> ClientSession:
|
||||||
|
"""Get a ClientSession that can be used with LinkPlay devices."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
if CONF_SESSION not in hass.data[DOMAIN]:
|
||||||
|
clientsession: ClientSession = await async_create_unverified_client_session()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_close_websession(event: Event) -> None:
|
||||||
|
"""Close websession."""
|
||||||
|
clientsession.detach()
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_websession)
|
||||||
|
hass.data[DOMAIN][CONF_SESSION] = clientsession
|
||||||
|
return clientsession
|
||||||
|
|
||||||
|
session: ClientSession = hass.data[DOMAIN][CONF_SESSION]
|
||||||
|
return session
|
||||||
|
@ -2323,7 +2323,7 @@ python-juicenet==1.1.0
|
|||||||
python-kasa[speedups]==0.7.2
|
python-kasa[speedups]==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.linkplay
|
# homeassistant.components.linkplay
|
||||||
python-linkplay==0.0.8
|
python-linkplay==0.0.9
|
||||||
|
|
||||||
# homeassistant.components.lirc
|
# homeassistant.components.lirc
|
||||||
# python-lirc==1.2.3
|
# python-lirc==1.2.3
|
||||||
|
@ -1844,7 +1844,7 @@ python-juicenet==1.1.0
|
|||||||
python-kasa[speedups]==0.7.2
|
python-kasa[speedups]==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.linkplay
|
# homeassistant.components.linkplay
|
||||||
python-linkplay==0.0.8
|
python-linkplay==0.0.9
|
||||||
|
|
||||||
# homeassistant.components.matter
|
# homeassistant.components.matter
|
||||||
python-matter-server==6.3.0
|
python-matter-server==6.3.0
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
from linkplay.bridge import LinkPlayBridge, LinkPlayDevice
|
from linkplay.bridge import LinkPlayBridge, LinkPlayDevice
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -14,11 +15,15 @@ NAME = "Smart Zone 1_54B9"
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_linkplay_factory_bridge() -> Generator[AsyncMock]:
|
def mock_linkplay_factory_bridge() -> Generator[AsyncMock]:
|
||||||
"""Mock for linkplay_factory_bridge."""
|
"""Mock for linkplay_factory_httpapi_bridge."""
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.linkplay.config_flow.linkplay_factory_bridge"
|
"homeassistant.components.linkplay.config_flow.async_get_client_session",
|
||||||
|
return_value=AsyncMock(spec=ClientSession),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.linkplay.config_flow.linkplay_factory_httpapi_bridge",
|
||||||
) as factory,
|
) as factory,
|
||||||
):
|
):
|
||||||
bridge = AsyncMock(spec=LinkPlayBridge)
|
bridge = AsyncMock(spec=LinkPlayBridge)
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from linkplay.exceptions import LinkPlayRequestException
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.linkplay.const import DOMAIN
|
from homeassistant.components.linkplay.const import DOMAIN
|
||||||
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||||
@ -47,10 +50,9 @@ ZEROCONF_DISCOVERY_RE_ENTRY = ZeroconfServiceInfo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_linkplay_factory_bridge", "mock_setup_entry")
|
||||||
async def test_user_flow(
|
async def test_user_flow(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_linkplay_factory_bridge: AsyncMock,
|
|
||||||
mock_setup_entry: AsyncMock,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test user setup config flow."""
|
"""Test user setup config flow."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -74,10 +76,9 @@ async def test_user_flow(
|
|||||||
assert result["result"].unique_id == UUID
|
assert result["result"].unique_id == UUID
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_linkplay_factory_bridge")
|
||||||
async def test_user_flow_re_entry(
|
async def test_user_flow_re_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_linkplay_factory_bridge: AsyncMock,
|
|
||||||
mock_setup_entry: AsyncMock,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test user setup config flow when an entry with the same unique id already exists."""
|
"""Test user setup config flow when an entry with the same unique id already exists."""
|
||||||
|
|
||||||
@ -105,10 +106,9 @@ async def test_user_flow_re_entry(
|
|||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_linkplay_factory_bridge", "mock_setup_entry")
|
||||||
async def test_zeroconf_flow(
|
async def test_zeroconf_flow(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_linkplay_factory_bridge: AsyncMock,
|
|
||||||
mock_setup_entry: AsyncMock,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Zeroconf flow."""
|
"""Test Zeroconf flow."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -133,10 +133,9 @@ async def test_zeroconf_flow(
|
|||||||
assert result["result"].unique_id == UUID
|
assert result["result"].unique_id == UUID
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_linkplay_factory_bridge")
|
||||||
async def test_zeroconf_flow_re_entry(
|
async def test_zeroconf_flow_re_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_linkplay_factory_bridge: AsyncMock,
|
|
||||||
mock_setup_entry: AsyncMock,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Zeroconf flow when an entry with the same unique id already exists."""
|
"""Test Zeroconf flow when an entry with the same unique id already exists."""
|
||||||
|
|
||||||
@ -160,16 +159,35 @@ async def test_zeroconf_flow_re_entry(
|
|||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_flow_errors(
|
@pytest.mark.usefixtures("mock_setup_entry")
|
||||||
|
async def test_zeroconf_flow_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_linkplay_factory_bridge: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test flow when the device discovered through Zeroconf cannot be reached."""
|
||||||
|
|
||||||
|
# Temporarily make the mock_linkplay_factory_bridge throw an exception
|
||||||
|
mock_linkplay_factory_bridge.side_effect = (LinkPlayRequestException("Error"),)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_ZEROCONF},
|
||||||
|
data=ZEROCONF_DISCOVERY,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "cannot_connect"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_setup_entry")
|
||||||
|
async def test_user_flow_errors(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_linkplay_factory_bridge: AsyncMock,
|
mock_linkplay_factory_bridge: AsyncMock,
|
||||||
mock_setup_entry: AsyncMock,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test flow when the device cannot be reached."""
|
"""Test flow when the device cannot be reached."""
|
||||||
|
|
||||||
# Temporarily store bridge in a separate variable and set factory to return None
|
# Temporarily make the mock_linkplay_factory_bridge throw an exception
|
||||||
bridge = mock_linkplay_factory_bridge.return_value
|
mock_linkplay_factory_bridge.side_effect = (LinkPlayRequestException("Error"),)
|
||||||
mock_linkplay_factory_bridge.return_value = None
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -188,8 +206,8 @@ async def test_flow_errors(
|
|||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "user"
|
||||||
assert result["errors"] == {"base": "cannot_connect"}
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
# Make linkplay_factory_bridge return a mock bridge again
|
# Make mock_linkplay_factory_bridge_exception no longer throw an exception
|
||||||
mock_linkplay_factory_bridge.return_value = bridge
|
mock_linkplay_factory_bridge.side_effect = None
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user