mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +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
|
||||
from typing import Any
|
||||
|
||||
from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV
|
||||
from haphilipsjs import (
|
||||
DEFAULT_API_VERSION,
|
||||
ConnectionFailure,
|
||||
GeneralFailure,
|
||||
PairingFailure,
|
||||
PhilipsTV,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
@ -18,16 +24,18 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import (
|
||||
CONF_API_VERSION,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PIN,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaFlowFormStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import LOGGER
|
||||
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):
|
||||
"""Handle a config flow for Philips TV."""
|
||||
|
||||
@ -81,6 +74,38 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._hub: PhilipsTV | None = 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:
|
||||
system = self._current[CONF_SYSTEM]
|
||||
if self.source == SOURCE_REAUTH:
|
||||
@ -154,6 +179,43 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._current[CONF_API_VERSION] = entry_data[CONF_API_VERSION]
|
||||
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(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@ -162,28 +224,14 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input:
|
||||
self._current = user_input
|
||||
try:
|
||||
hub = await _validate_input(
|
||||
self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION]
|
||||
await self._async_attempt_prepare(
|
||||
user_input[CONF_HOST], user_input[CONF_API_VERSION], False
|
||||
)
|
||||
except ConnectionFailure as exc:
|
||||
except GeneralFailure as exc:
|
||||
LOGGER.error(exc)
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
if serialnumber := hub.system.get("serialnumber"):
|
||||
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()
|
||||
return await self._async_attempt_add()
|
||||
|
||||
schema = self.add_suggested_values_to_schema(USER_SCHEMA, self._current)
|
||||
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",
|
||||
"iot_class": "local_polling",
|
||||
"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": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
@ -7,6 +8,10 @@
|
||||
"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": {
|
||||
"title": "Pair",
|
||||
"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",
|
||||
},
|
||||
],
|
||||
"_philipstv_rpc._tcp.local.": [
|
||||
{
|
||||
"domain": "philips_js",
|
||||
},
|
||||
],
|
||||
"_philipstv_s_rpc._tcp.local.": [
|
||||
{
|
||||
"domain": "philips_js",
|
||||
},
|
||||
],
|
||||
"_plexmediasvr._tcp.local.": [
|
||||
{
|
||||
"domain": "plex",
|
||||
|
@ -5,6 +5,7 @@ MOCK_NAME = "Philips TV"
|
||||
|
||||
MOCK_USERNAME = "mock_user"
|
||||
MOCK_PASSWORD = "mock_password"
|
||||
MOCK_HOSTNAME = "mock_hostname"
|
||||
|
||||
MOCK_SYSTEM = {
|
||||
"menulanguage": "English",
|
||||
|
@ -38,6 +38,7 @@ def mock_tv():
|
||||
tv.application = None
|
||||
tv.applications = {}
|
||||
tv.system = MOCK_SYSTEM
|
||||
tv.name = MOCK_NAME
|
||||
tv.api_version = 1
|
||||
tv.api_version_detected = None
|
||||
tv.on = True
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test the Philips TV config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import ANY
|
||||
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import (
|
||||
MOCK_CONFIG,
|
||||
MOCK_CONFIG_PAIRED,
|
||||
MOCK_HOSTNAME,
|
||||
MOCK_NAME,
|
||||
MOCK_PASSWORD,
|
||||
MOCK_SYSTEM,
|
||||
MOCK_SYSTEM_UNPAIRED,
|
||||
@ -33,6 +37,7 @@ async def mock_tv_pairable(mock_tv):
|
||||
mock_tv.api_version = 6
|
||||
mock_tv.api_version_detected = 6
|
||||
mock_tv.secured_transport = True
|
||||
mock_tv.name = MOCK_NAME
|
||||
|
||||
mock_tv.pairRequest.return_value = {}
|
||||
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"}
|
||||
|
||||
|
||||
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:
|
||||
"""Test we get the form."""
|
||||
mock_tv = mock_tv_pairable
|
||||
@ -143,7 +133,13 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry)
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"context": {"source": "user", "unique_id": "ABCDEFGHIJKLF"},
|
||||
"context": {
|
||||
"source": "user",
|
||||
"unique_id": "ABCDEFGHIJKLF",
|
||||
"title_placeholders": {
|
||||
"name": "Philips TV",
|
||||
},
|
||||
},
|
||||
"flow_id": ANY,
|
||||
"type": "create_entry",
|
||||
"description": None,
|
||||
@ -258,3 +254,67 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
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