mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Add zeroconf discovery to philips_js (#147913)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
6e607ffa01
commit
470baa782e
@ -6,7 +6,13 @@ from collections.abc import Mapping
|
|||||||
import platform
|
import platform
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV
|
from haphilipsjs import (
|
||||||
|
DEFAULT_API_VERSION,
|
||||||
|
ConnectionFailure,
|
||||||
|
GeneralFailure,
|
||||||
|
PairingFailure,
|
||||||
|
PhilipsTV,
|
||||||
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
@ -18,16 +24,18 @@ from homeassistant.config_entries import (
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_VERSION,
|
CONF_API_VERSION,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import selector
|
from homeassistant.helpers import selector
|
||||||
from homeassistant.helpers.schema_config_entry_flow import (
|
from homeassistant.helpers.schema_config_entry_flow import (
|
||||||
SchemaFlowFormStep,
|
SchemaFlowFormStep,
|
||||||
SchemaOptionsFlowHandler,
|
SchemaOptionsFlowHandler,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
from . import LOGGER
|
from . import LOGGER
|
||||||
from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN
|
from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN
|
||||||
@ -54,21 +62,6 @@ OPTIONS_FLOW = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def _validate_input(
|
|
||||||
hass: HomeAssistant, host: str, api_version: int
|
|
||||||
) -> PhilipsTV:
|
|
||||||
"""Validate the user input allows us to connect."""
|
|
||||||
hub = PhilipsTV(host, api_version)
|
|
||||||
|
|
||||||
await hub.getSystem()
|
|
||||||
await hub.setTransport(hub.secured_transport)
|
|
||||||
|
|
||||||
if not hub.system:
|
|
||||||
raise ConnectionFailure("System data is empty")
|
|
||||||
|
|
||||||
return hub
|
|
||||||
|
|
||||||
|
|
||||||
class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Philips TV."""
|
"""Handle a config flow for Philips TV."""
|
||||||
|
|
||||||
@ -81,6 +74,38 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self._hub: PhilipsTV | None = None
|
self._hub: PhilipsTV | None = None
|
||||||
self._pair_state: Any = None
|
self._pair_state: Any = None
|
||||||
|
|
||||||
|
async def _async_attempt_prepare(
|
||||||
|
self, host: str, api_version: int, secured_transport: bool
|
||||||
|
) -> None:
|
||||||
|
hub = PhilipsTV(
|
||||||
|
host, api_version=api_version, secured_transport=secured_transport
|
||||||
|
)
|
||||||
|
|
||||||
|
await hub.getSystem()
|
||||||
|
await hub.setTransport(hub.secured_transport)
|
||||||
|
|
||||||
|
if not hub.system or not hub.name:
|
||||||
|
raise ConnectionFailure("System data or name is empty")
|
||||||
|
|
||||||
|
self._hub = hub
|
||||||
|
self._current[CONF_HOST] = host
|
||||||
|
self._current[CONF_SYSTEM] = hub.system
|
||||||
|
self._current[CONF_API_VERSION] = hub.api_version
|
||||||
|
self.context.update({"title_placeholders": {CONF_NAME: hub.name}})
|
||||||
|
|
||||||
|
if serialnumber := hub.system.get("serialnumber"):
|
||||||
|
await self.async_set_unique_id(serialnumber)
|
||||||
|
if self.source != SOURCE_REAUTH:
|
||||||
|
self._abort_if_unique_id_configured(
|
||||||
|
updates=self._current, reload_on_update=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_attempt_add(self) -> ConfigFlowResult:
|
||||||
|
assert self._hub
|
||||||
|
if self._hub.pairing_type == "digest_auth_pairing":
|
||||||
|
return await self.async_step_pair()
|
||||||
|
return await self._async_create_current()
|
||||||
|
|
||||||
async def _async_create_current(self) -> ConfigFlowResult:
|
async def _async_create_current(self) -> ConfigFlowResult:
|
||||||
system = self._current[CONF_SYSTEM]
|
system = self._current[CONF_SYSTEM]
|
||||||
if self.source == SOURCE_REAUTH:
|
if self.source == SOURCE_REAUTH:
|
||||||
@ -154,6 +179,43 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self._current[CONF_API_VERSION] = entry_data[CONF_API_VERSION]
|
self._current[CONF_API_VERSION] = entry_data[CONF_API_VERSION]
|
||||||
return await self.async_step_user()
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: ZeroconfServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle zeroconf discovery."""
|
||||||
|
|
||||||
|
LOGGER.debug(
|
||||||
|
"Checking discovered device: {discovery_info.name} on {discovery_info.host}"
|
||||||
|
)
|
||||||
|
|
||||||
|
secured_transport = discovery_info.type == "_philipstv_s_rpc._tcp.local."
|
||||||
|
api_version = 6 if secured_transport else DEFAULT_API_VERSION
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self._async_attempt_prepare(
|
||||||
|
discovery_info.host, api_version, secured_transport
|
||||||
|
)
|
||||||
|
except GeneralFailure:
|
||||||
|
LOGGER.debug("Failed to get system info from discovery", exc_info=True)
|
||||||
|
return self.async_abort(reason="discovery_failure")
|
||||||
|
|
||||||
|
return await self.async_step_zeroconf_confirm()
|
||||||
|
|
||||||
|
async def async_step_zeroconf_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initiated by zeroconf."""
|
||||||
|
if user_input is not None:
|
||||||
|
return await self._async_attempt_add()
|
||||||
|
|
||||||
|
name = self.context.get("title_placeholders", {CONF_NAME: "Philips TV"})[
|
||||||
|
CONF_NAME
|
||||||
|
]
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="zeroconf_confirm",
|
||||||
|
description_placeholders={CONF_NAME: name},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@ -162,28 +224,14 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input:
|
if user_input:
|
||||||
self._current = user_input
|
self._current = user_input
|
||||||
try:
|
try:
|
||||||
hub = await _validate_input(
|
await self._async_attempt_prepare(
|
||||||
self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION]
|
user_input[CONF_HOST], user_input[CONF_API_VERSION], False
|
||||||
)
|
)
|
||||||
except ConnectionFailure as exc:
|
except GeneralFailure as exc:
|
||||||
LOGGER.error(exc)
|
LOGGER.error(exc)
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
LOGGER.exception("Unexpected exception")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
else:
|
else:
|
||||||
if serialnumber := hub.system.get("serialnumber"):
|
return await self._async_attempt_add()
|
||||||
await self.async_set_unique_id(serialnumber)
|
|
||||||
if self.source != SOURCE_REAUTH:
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
self._current[CONF_SYSTEM] = hub.system
|
|
||||||
self._current[CONF_API_VERSION] = hub.api_version
|
|
||||||
self._hub = hub
|
|
||||||
|
|
||||||
if hub.pairing_type == "digest_auth_pairing":
|
|
||||||
return await self.async_step_pair()
|
|
||||||
return await self._async_create_current()
|
|
||||||
|
|
||||||
schema = self.add_suggested_values_to_schema(USER_SCHEMA, self._current)
|
schema = self.add_suggested_values_to_schema(USER_SCHEMA, self._current)
|
||||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||||
|
@ -6,5 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["haphilipsjs"],
|
"loggers": ["haphilipsjs"],
|
||||||
"requirements": ["ha-philipsjs==3.2.2"]
|
"requirements": ["ha-philipsjs==3.2.2"],
|
||||||
|
"zeroconf": ["_philipstv_s_rpc._tcp.local.", "_philipstv_rpc._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"flow_title": "{name}",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
@ -7,6 +8,10 @@
|
|||||||
"api_version": "API Version"
|
"api_version": "API Version"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"zeroconf_confirm": {
|
||||||
|
"title": "Discovered Philips TV",
|
||||||
|
"description": "Do you want to add the TV ({name}) to Home Assistant? It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen."
|
||||||
|
},
|
||||||
"pair": {
|
"pair": {
|
||||||
"title": "Pair",
|
"title": "Pair",
|
||||||
"description": "Enter the PIN displayed on your TV",
|
"description": "Enter the PIN displayed on your TV",
|
||||||
|
10
homeassistant/generated/zeroconf.py
generated
10
homeassistant/generated/zeroconf.py
generated
@ -771,6 +771,16 @@ ZEROCONF = {
|
|||||||
"domain": "onewire",
|
"domain": "onewire",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"_philipstv_rpc._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "philips_js",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"_philipstv_s_rpc._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "philips_js",
|
||||||
|
},
|
||||||
|
],
|
||||||
"_plexmediasvr._tcp.local.": [
|
"_plexmediasvr._tcp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "plex",
|
"domain": "plex",
|
||||||
|
@ -5,6 +5,7 @@ MOCK_NAME = "Philips TV"
|
|||||||
|
|
||||||
MOCK_USERNAME = "mock_user"
|
MOCK_USERNAME = "mock_user"
|
||||||
MOCK_PASSWORD = "mock_password"
|
MOCK_PASSWORD = "mock_password"
|
||||||
|
MOCK_HOSTNAME = "mock_hostname"
|
||||||
|
|
||||||
MOCK_SYSTEM = {
|
MOCK_SYSTEM = {
|
||||||
"menulanguage": "English",
|
"menulanguage": "English",
|
||||||
|
@ -38,6 +38,7 @@ def mock_tv():
|
|||||||
tv.application = None
|
tv.application = None
|
||||||
tv.applications = {}
|
tv.applications = {}
|
||||||
tv.system = MOCK_SYSTEM
|
tv.system = MOCK_SYSTEM
|
||||||
|
tv.name = MOCK_NAME
|
||||||
tv.api_version = 1
|
tv.api_version = 1
|
||||||
tv.api_version_detected = None
|
tv.api_version_detected = None
|
||||||
tv.on = True
|
tv.on = True
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Test the Philips TV config flow."""
|
"""Test the Philips TV config flow."""
|
||||||
|
|
||||||
|
from ipaddress import ip_address
|
||||||
from unittest.mock import ANY
|
from unittest.mock import ANY
|
||||||
|
|
||||||
from haphilipsjs import PairingFailure
|
from haphilipsjs import PairingFailure
|
||||||
@ -9,10 +10,13 @@ from homeassistant import config_entries
|
|||||||
from homeassistant.components.philips_js.const import CONF_ALLOW_NOTIFY, DOMAIN
|
from homeassistant.components.philips_js.const import CONF_ALLOW_NOTIFY, DOMAIN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
MOCK_CONFIG,
|
MOCK_CONFIG,
|
||||||
MOCK_CONFIG_PAIRED,
|
MOCK_CONFIG_PAIRED,
|
||||||
|
MOCK_HOSTNAME,
|
||||||
|
MOCK_NAME,
|
||||||
MOCK_PASSWORD,
|
MOCK_PASSWORD,
|
||||||
MOCK_SYSTEM,
|
MOCK_SYSTEM,
|
||||||
MOCK_SYSTEM_UNPAIRED,
|
MOCK_SYSTEM_UNPAIRED,
|
||||||
@ -33,6 +37,7 @@ async def mock_tv_pairable(mock_tv):
|
|||||||
mock_tv.api_version = 6
|
mock_tv.api_version = 6
|
||||||
mock_tv.api_version_detected = 6
|
mock_tv.api_version_detected = 6
|
||||||
mock_tv.secured_transport = True
|
mock_tv.secured_transport = True
|
||||||
|
mock_tv.name = MOCK_NAME
|
||||||
|
|
||||||
mock_tv.pairRequest.return_value = {}
|
mock_tv.pairRequest.return_value = {}
|
||||||
mock_tv.pairGrant.return_value = MOCK_USERNAME, MOCK_PASSWORD
|
mock_tv.pairGrant.return_value = MOCK_USERNAME, MOCK_PASSWORD
|
||||||
@ -102,21 +107,6 @@ async def test_form_cannot_connect(hass: HomeAssistant, mock_tv) -> None:
|
|||||||
assert result["errors"] == {"base": "cannot_connect"}
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
async def test_form_unexpected_error(hass: HomeAssistant, mock_tv) -> None:
|
|
||||||
"""Test we handle unexpected exceptions."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_tv.getSystem.side_effect = Exception("Unexpected exception")
|
|
||||||
result = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"], MOCK_USERINPUT
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
|
||||||
assert result["errors"] == {"base": "unknown"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) -> None:
|
async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) -> None:
|
||||||
"""Test we get the form."""
|
"""Test we get the form."""
|
||||||
mock_tv = mock_tv_pairable
|
mock_tv = mock_tv_pairable
|
||||||
@ -143,7 +133,13 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry)
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result == {
|
assert result == {
|
||||||
"context": {"source": "user", "unique_id": "ABCDEFGHIJKLF"},
|
"context": {
|
||||||
|
"source": "user",
|
||||||
|
"unique_id": "ABCDEFGHIJKLF",
|
||||||
|
"title_placeholders": {
|
||||||
|
"name": "Philips TV",
|
||||||
|
},
|
||||||
|
},
|
||||||
"flow_id": ANY,
|
"flow_id": ANY,
|
||||||
"type": "create_entry",
|
"type": "create_entry",
|
||||||
"description": None,
|
"description": None,
|
||||||
@ -258,3 +254,67 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert config_entry.options == {CONF_ALLOW_NOTIFY: True}
|
assert config_entry.options == {CONF_ALLOW_NOTIFY: True}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("secured_transport", "discovery_type"),
|
||||||
|
[(True, "_philipstv_s_rpc._tcp.local."), (False, "_philipstv_rpc._tcp.local.")],
|
||||||
|
)
|
||||||
|
async def test_zeroconf_discovery(
|
||||||
|
hass: HomeAssistant, mock_tv_pairable, secured_transport, discovery_type
|
||||||
|
) -> None:
|
||||||
|
"""Test we can setup from zeroconf discovery."""
|
||||||
|
|
||||||
|
mock_tv_pairable.secured_transport = secured_transport
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data=ZeroconfServiceInfo(
|
||||||
|
ip_address=ip_address("127.0.0.1"),
|
||||||
|
ip_addresses=[ip_address("127.0.0.1")],
|
||||||
|
hostname=MOCK_HOSTNAME,
|
||||||
|
name=MOCK_NAME,
|
||||||
|
port=None,
|
||||||
|
properties={},
|
||||||
|
type=discovery_type,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] is None
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
mock_tv_pairable.setTransport.assert_called_with(secured_transport)
|
||||||
|
mock_tv_pairable.pairRequest.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_probe_failed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_tv_pairable,
|
||||||
|
) -> None:
|
||||||
|
"""Test we can setup from zeroconf discovery."""
|
||||||
|
|
||||||
|
mock_tv_pairable.system = None
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data=ZeroconfServiceInfo(
|
||||||
|
ip_address=ip_address("127.0.0.1"),
|
||||||
|
ip_addresses=[ip_address("127.0.0.1")],
|
||||||
|
hostname=MOCK_HOSTNAME,
|
||||||
|
name=MOCK_NAME,
|
||||||
|
port=None,
|
||||||
|
properties={},
|
||||||
|
type="_philipstv_s_rpc._tcp.local.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "discovery_failure"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user