Add HTTPS support for Fully Kiosk (#89592)

* Add HTTPS support for Fully Kiosk with optional certificate verification.

* All pytests passing.

* Better readability for url parameter of DeviceInfo

* All pytests passing with latest fixes from upstream

* Removing fully_kiosk/translations

* Rebasing

* Added extra error detail when the integration config flow fails

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
r01k 2023-11-13 09:40:57 -07:00 committed by GitHub
parent 39c81cb4b1
commit 7ef47da27d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 103 additions and 13 deletions

View File

@ -12,7 +12,13 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_SSL,
CONF_VERIFY_SSL,
)
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
@ -31,13 +37,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._discovered_device_info: dict[str, Any] = {}
async def _create_entry(
self, host: str, user_input: dict[str, Any], errors: dict[str, str]
self,
host: str,
user_input: dict[str, Any],
errors: dict[str, str],
description_placeholders: dict[str, str] | Any = None,
) -> FlowResult | None:
fully = FullyKiosk(
async_get_clientsession(self.hass),
host,
DEFAULT_PORT,
user_input[CONF_PASSWORD],
use_ssl=user_input[CONF_SSL],
verify_ssl=user_input[CONF_VERIFY_SSL],
)
try:
@ -50,10 +62,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) as error:
LOGGER.debug(error.args, exc_info=True)
errors["base"] = "cannot_connect"
description_placeholders["error_detail"] = str(error.args)
return None
except Exception as error: # pylint: disable=broad-except
LOGGER.exception("Unexpected exception: %s", error)
errors["base"] = "unknown"
description_placeholders["error_detail"] = str(error.args)
return None
await self.async_set_unique_id(device_info["deviceID"], raise_on_progress=False)
@ -64,6 +78,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_HOST: host,
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_MAC: format_mac(device_info["Mac"]),
CONF_SSL: user_input[CONF_SSL],
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
},
)
@ -72,8 +88,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
placeholders: dict[str, str] = {}
if user_input is not None:
result = await self._create_entry(user_input[CONF_HOST], user_input, errors)
result = await self._create_entry(
user_input[CONF_HOST], user_input, errors, placeholders
)
if result:
return result
@ -83,8 +102,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_SSL, default=False): bool,
vol.Optional(CONF_VERIFY_SSL, default=False): bool,
}
),
description_placeholders=placeholders,
errors=errors,
)
@ -127,6 +149,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_SSL, default=False): bool,
vol.Optional(CONF_VERIFY_SSL, default=False): bool,
}
),
description_placeholders=placeholders,

View File

@ -6,7 +6,7 @@ from fullykiosk import FullyKiosk
from fullykiosk.exceptions import FullyKioskError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -24,6 +24,8 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
entry.data[CONF_HOST],
DEFAULT_PORT,
entry.data[CONF_PASSWORD],
use_ssl=entry.data[CONF_SSL],
verify_ssl=entry.data[CONF_VERIFY_SSL],
)
super().__init__(
hass,
@ -31,6 +33,7 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
name=entry.data[CONF_HOST],
update_interval=UPDATE_INTERVAL,
)
self.use_ssl = entry.data[CONF_SSL]
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""

View File

@ -1,6 +1,8 @@
"""Base entity for the Fully Kiosk Browser integration."""
from __future__ import annotations
from yarl import URL
from homeassistant.const import ATTR_CONNECTIONS
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import Entity
@ -30,13 +32,20 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit
def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None:
"""Initialize the Fully Kiosk Browser entity."""
super().__init__(coordinator=coordinator)
url = URL.build(
scheme="https" if coordinator.use_ssl else "http",
host=coordinator.data["ip4"],
port=2323,
)
device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.data["deviceID"])},
name=coordinator.data["deviceName"],
manufacturer=coordinator.data["deviceManufacturer"],
model=coordinator.data["deviceModel"],
sw_version=coordinator.data["appVersionName"],
configuration_url=f"http://{coordinator.data['ip4']}:2323",
configuration_url=str(url),
)
if "Mac" in coordinator.data and valid_global_mac_address(
coordinator.data["Mac"]

View File

@ -10,13 +10,15 @@
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]"
"password": "[%key:common::config_flow::data::password%]",
"ssl": "[%key:common::config_flow::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
"cannot_connect": "Cannot connect. Details: {error_detail}",
"unknown": "Unknown. Details: {error_detail}"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"

View File

@ -8,7 +8,13 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.fully_kiosk.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_SSL,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
@ -24,6 +30,8 @@ def mock_config_entry() -> MockConfigEntry:
CONF_HOST: "127.0.0.1",
CONF_PASSWORD: "mocked-password",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_SSL: False,
CONF_VERIFY_SSL: False,
},
unique_id="12345",
)

View File

@ -10,7 +10,13 @@ import pytest
from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.components.fully_kiosk.const import DOMAIN
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_MQTT, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_SSL,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
@ -35,6 +41,8 @@ async def test_user_flow(
{
CONF_HOST: "1.1.1.1",
CONF_PASSWORD: "test-password",
CONF_SSL: False,
CONF_VERIFY_SSL: False,
},
)
@ -44,6 +52,8 @@ async def test_user_flow(
CONF_HOST: "1.1.1.1",
CONF_PASSWORD: "test-password",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_SSL: False,
CONF_VERIFY_SSL: False,
}
assert "result" in result2
assert result2["result"].unique_id == "12345"
@ -76,7 +86,13 @@ async def test_errors(
mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = side_effect
result2 = await hass.config_entries.flow.async_configure(
flow_id, user_input={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password"}
flow_id,
user_input={
CONF_HOST: "1.1.1.1",
CONF_PASSWORD: "test-password",
CONF_SSL: False,
CONF_VERIFY_SSL: False,
},
)
assert result2.get("type") == FlowResultType.FORM
@ -88,7 +104,13 @@ async def test_errors(
mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = None
result3 = await hass.config_entries.flow.async_configure(
flow_id, user_input={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password"}
flow_id,
user_input={
CONF_HOST: "1.1.1.1",
CONF_PASSWORD: "test-password",
CONF_SSL: True,
CONF_VERIFY_SSL: False,
},
)
assert result3.get("type") == FlowResultType.CREATE_ENTRY
@ -97,6 +119,8 @@ async def test_errors(
CONF_HOST: "1.1.1.1",
CONF_PASSWORD: "test-password",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_SSL: True,
CONF_VERIFY_SSL: False,
}
assert "result" in result3
assert result3["result"].unique_id == "12345"
@ -124,6 +148,8 @@ async def test_duplicate_updates_existing_entry(
{
CONF_HOST: "1.1.1.1",
CONF_PASSWORD: "test-password",
CONF_SSL: True,
CONF_VERIFY_SSL: True,
},
)
@ -133,6 +159,8 @@ async def test_duplicate_updates_existing_entry(
CONF_HOST: "1.1.1.1",
CONF_PASSWORD: "test-password",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_SSL: True,
CONF_VERIFY_SSL: True,
}
assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1
@ -161,6 +189,8 @@ async def test_dhcp_discovery_updates_entry(
CONF_HOST: "127.0.0.2",
CONF_PASSWORD: "mocked-password",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_SSL: False,
CONF_VERIFY_SSL: False,
}
@ -212,6 +242,8 @@ async def test_mqtt_discovery_flow(
result["flow_id"],
{
CONF_PASSWORD: "test-password",
CONF_SSL: False,
CONF_VERIFY_SSL: False,
},
)
@ -222,6 +254,8 @@ async def test_mqtt_discovery_flow(
CONF_HOST: "192.168.1.234",
CONF_PASSWORD: "test-password",
CONF_MAC: "aa:bb:cc:dd:ee:ff",
CONF_SSL: False,
CONF_VERIFY_SSL: False,
}
assert "result" in confirmResult
assert confirmResult["result"].unique_id == "12345"

View File

@ -9,7 +9,13 @@ import pytest
from homeassistant.components.fully_kiosk.const import DOMAIN
from homeassistant.components.fully_kiosk.entity import valid_global_mac_address
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_SSL,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@ -92,6 +98,8 @@ async def test_multiple_kiosk_with_empty_mac(
CONF_HOST: "127.0.0.1",
CONF_PASSWORD: "mocked-password",
CONF_MAC: "",
CONF_SSL: False,
CONF_VERIFY_SSL: False,
},
unique_id="111111",
)
@ -105,6 +113,8 @@ async def test_multiple_kiosk_with_empty_mac(
CONF_HOST: "127.0.0.2",
CONF_PASSWORD: "mocked-password",
CONF_MAC: "",
CONF_SSL: True,
CONF_VERIFY_SSL: False,
},
unique_id="22222",
)