diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py index 785948f5632..f324cdbe0a0 100644 --- a/homeassistant/components/fully_kiosk/config_flow.py +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import json from typing import Any from aiohttp.client_exceptions import ClientConnectorError @@ -16,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from .const import DEFAULT_PORT, DOMAIN, LOGGER @@ -25,39 +27,56 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_device_info: dict[str, Any] = {} + + async def _create_entry( + self, host: str, user_input: dict[str, Any], errors: dict[str, str] + ) -> FlowResult | None: + fully = FullyKiosk( + async_get_clientsession(self.hass), + host, + DEFAULT_PORT, + user_input[CONF_PASSWORD], + ) + + try: + async with timeout(15): + device_info = await fully.getDeviceInfo() + except ( + ClientConnectorError, + FullyKioskError, + asyncio.TimeoutError, + ) as error: + LOGGER.debug(error.args, exc_info=True) + errors["base"] = "cannot_connect" + return None + except Exception as error: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception: %s", error) + errors["base"] = "unknown" + return None + + await self.async_set_unique_id(device_info["deviceID"]) + self._abort_if_unique_id_configured(updates=user_input) + return self.async_create_entry( + title=device_info["deviceName"], + data={ + CONF_HOST: host, + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_MAC: format_mac(device_info["Mac"]), + }, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: - fully = FullyKiosk( - async_get_clientsession(self.hass), - user_input[CONF_HOST], - DEFAULT_PORT, - user_input[CONF_PASSWORD], - ) - - try: - async with timeout(15): - device_info = await fully.getDeviceInfo() - except ( - ClientConnectorError, - FullyKioskError, - asyncio.TimeoutError, - ) as error: - LOGGER.debug(error.args, exc_info=True) - errors["base"] = "cannot_connect" - except Exception as error: # pylint: disable=broad-except - LOGGER.exception("Unexpected exception: %s", error) - errors["base"] = "unknown" - else: - await self.async_set_unique_id(device_info["deviceID"]) - self._abort_if_unique_id_configured(updates=user_input) - return self.async_create_entry( - title=device_info["deviceName"], - data=user_input | {CONF_MAC: format_mac(device_info["Mac"])}, - ) + result = await self._create_entry(user_input[CONF_HOST], user_input, errors) + if result: + return result return self.async_show_form( step_id="user", @@ -86,3 +105,42 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") return self.async_abort(reason="unknown") + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + errors: dict[str, str] = {} + if user_input is not None: + result = await self._create_entry( + self.context[CONF_HOST], user_input, errors + ) + if result: + return result + + placeholders = { + "name": self._discovered_device_info["deviceName"], + CONF_HOST: self.context[CONF_HOST], + } + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders=placeholders, + errors=errors, + ) + + async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: + """Handle a flow initialized by MQTT discovery.""" + device_info: dict[str, Any] = json.loads(discovery_info.payload) + device_id: str = device_info["deviceId"] + await self.async_set_unique_id(device_id) + self._abort_if_unique_id_configured() + + self.context[CONF_HOST] = device_info["hostname4"] + self._discovered_device_info = device_info + return await self.async_step_discovery_confirm() diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index 7eba5d04312..f313a117c44 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -10,5 +10,6 @@ ], "documentation": "https://www.home-assistant.io/integrations/fullykiosk", "iot_class": "local_polling", + "mqtt": ["fully/deviceInfo/+"], "requirements": ["python-fullykiosk==0.0.12"] } diff --git a/homeassistant/components/fully_kiosk/strings.json b/homeassistant/components/fully_kiosk/strings.json index 873ebc661fb..a6442085683 100644 --- a/homeassistant/components/fully_kiosk/strings.json +++ b/homeassistant/components/fully_kiosk/strings.json @@ -1,6 +1,12 @@ { "config": { "step": { + "discovery_confirm": { + "description": "Do you want to set up {name} ({host})?", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]", diff --git a/homeassistant/generated/mqtt.py b/homeassistant/generated/mqtt.py index 4d4e47669c2..5d64546b91b 100644 --- a/homeassistant/generated/mqtt.py +++ b/homeassistant/generated/mqtt.py @@ -7,6 +7,9 @@ MQTT = { "dsmr_reader": [ "dsmr/#", ], + "fully_kiosk": [ + "fully/deviceInfo/+", + ], "tasmota": [ "tasmota/discovery/#", ], diff --git a/tests/components/fully_kiosk/fixtures/mqtt-discovery-deviceinfo.json b/tests/components/fully_kiosk/fixtures/mqtt-discovery-deviceinfo.json new file mode 100644 index 00000000000..1c8133647bd --- /dev/null +++ b/tests/components/fully_kiosk/fixtures/mqtt-discovery-deviceinfo.json @@ -0,0 +1,79 @@ +{ + "deviceId": "abcdef-123456", + "deviceName": "Amazon Fire", + "batteryLevel": 100, + "isPlugged": true, + "SSID": "\"freewifi\"", + "Mac": "aa:bb:cc:dd:ee:ff", + "ip4": "192.168.1.234", + "ip6": "FE80::1874:2EFF:FEA2:7848", + "hostname4": "192.168.1.234", + "hostname6": "fe80::1874:2eff:fea2:7848%p2p0", + "wifiSignalLevel": 7, + "isMobileDataEnabled": true, + "screenOrientation": 90, + "screenBrightness": 9, + "screenLocked": false, + "screenOn": true, + "batteryTemperature": 27, + "plugged": true, + "keyguardLocked": false, + "locale": "en_US", + "serial": "ABCDEF1234567890", + "build": "cm_douglas-userdebug 5.1.1 LMY49M 731a881f9d test-keys", + "androidVersion": "5.1.1", + "webviewUA": "Mozilla/5.0 (Linux; Android 5.1.1; KFDOWI Build/LMY49M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.117 Safari/537.36", + "motionDetectorStatus": 0, + "isDeviceAdmin": true, + "isDeviceOwner": false, + "internalStorageFreeSpace": 11675512832, + "internalStorageTotalSpace": 12938534912, + "ramUsedMemory": 1077755904, + "ramFreeMemory": 362373120, + "ramTotalMemory": 1440129024, + "appUsedMemory": 24720592, + "appFreeMemory": 59165440, + "appTotalMemory": 83886080, + "displayHeightPixels": 800, + "displayWidthPixels": 1280, + "isMenuOpen": false, + "topFragmentTag": "", + "isInDaydream": false, + "isRooted": true, + "isLicensed": true, + "isInScreensaver": false, + "kioskLocked": true, + "isInForcedSleep": false, + "maintenanceMode": false, + "kioskMode": true, + "startUrl": "https://homeassistant.local", + "currentTabIndex": 0, + "mqttConnected": true, + "appVersionCode": 875, + "appVersionName": "1.42.5", + "androidSdk": 22, + "deviceModel": "KFDOWI", + "deviceManufacturer": "amzn", + "foregroundApp": "de.ozerov.fully", + "currentPage": "https://homeassistant.local", + "lastAppStart": "8/13/2022 1:00:47 AM", + "sensorInfo": [ + { + "type": 8, + "name": "PROXIMITY", + "vendor": "MTK", + "version": 1, + "accuracy": -1 + }, + { + "type": 5, + "name": "LIGHT", + "vendor": "MTK", + "version": 1, + "accuracy": 3, + "values": [0, 0, 0], + "lastValuesTime": 1660435566561, + "lastAccuracyTime": 1660366847543 + } + ] +} diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 719d2442c51..8a6ae147600 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -9,15 +9,16 @@ import pytest from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fully_kiosk.const import DOMAIN -from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_MQTT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture -async def test_full_flow( +async def test_user_flow( hass: HomeAssistant, mock_fully_kiosk_config_flow: MagicMock, mock_setup_entry: AsyncMock, @@ -182,3 +183,48 @@ async def test_dhcp_unknown_device( assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "unknown" + + +async def test_mqtt_discovery_flow( + hass: HomeAssistant, + mock_fully_kiosk_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test MQTT discovery configuration flow.""" + payload = load_fixture("mqtt-discovery-deviceinfo.json", DOMAIN) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_MQTT}, + data=MqttServiceInfo( + topic="fully/deviceInfo/e1c9bb1-df31b345", + payload=payload, + qos=0, + retain=False, + subscribed_topic="fully/deviceInfo/+", + timestamp=None, + ), + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "discovery_confirm" + + confirmResult = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert confirmResult + assert confirmResult.get("type") == FlowResultType.CREATE_ENTRY + assert confirmResult.get("title") == "Test device" + assert confirmResult.get("data") == { + CONF_HOST: "192.168.1.234", + CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + } + assert "result" in confirmResult + assert confirmResult["result"].unique_id == "12345" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1