From c7876689eaa8fefdbc5b86b9955e0657031a6271 Mon Sep 17 00:00:00 2001 From: Manuel Dipolt Date: Mon, 29 May 2023 19:39:12 +0200 Subject: [PATCH] Add romy vacuum core integration --- .coveragerc | 3 + .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/romy/__init__.py | 44 ++++ homeassistant/components/romy/config_flow.py | 128 +++++++++++ homeassistant/components/romy/const.py | 14 ++ homeassistant/components/romy/coordinator.py | 22 ++ homeassistant/components/romy/manifest.json | 11 + homeassistant/components/romy/strings.json | 21 ++ homeassistant/components/romy/vacuum.py | 212 ++++++++++++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + homeassistant/generated/zeroconf.py | 5 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/romy/__init__.py | 1 + tests/components/romy/test_config_flow.py | 214 +++++++++++++++++++ 18 files changed, 701 insertions(+) create mode 100644 homeassistant/components/romy/__init__.py create mode 100644 homeassistant/components/romy/config_flow.py create mode 100644 homeassistant/components/romy/const.py create mode 100644 homeassistant/components/romy/coordinator.py create mode 100644 homeassistant/components/romy/manifest.json create mode 100644 homeassistant/components/romy/strings.json create mode 100644 homeassistant/components/romy/vacuum.py create mode 100644 tests/components/romy/__init__.py create mode 100644 tests/components/romy/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 928061407b8cef..df9cdaa96078f4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1004,6 +1004,9 @@ omit = homeassistant/components/ripple/sensor.py homeassistant/components/roborock/coordinator.py homeassistant/components/rocketchat/notify.py + homeassistant/components/romy/__init__.py + homeassistant/components/romy/coordinator.py + homeassistant/components/romy/vacuum.py homeassistant/components/roomba/__init__.py homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/braava.py diff --git a/.strict-typing b/.strict-typing index 801827df6dcc5d..887606aad786d7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -265,6 +265,7 @@ homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* +homeassistant.components.romy.* homeassistant.components.rpi_power.* homeassistant.components.rss_feed_template.* homeassistant.components.rtsp_to_webrtc.* diff --git a/CODEOWNERS b/CODEOWNERS index 3ca8df7ec16b9d..1be6463a5d3b3c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1022,6 +1022,8 @@ build.json @home-assistant/supervisor /tests/components/roborock/ @humbertogontijo @Lash-L /homeassistant/components/roku/ @ctalkington /tests/components/roku/ @ctalkington +/homeassistant/components/romy/ @xeniter +/tests/components/romy/ @xeniter /homeassistant/components/roomba/ @pschmitt @cyr-ius @shenxn /tests/components/roomba/ @pschmitt @cyr-ius @shenxn /homeassistant/components/roon/ @pavoni diff --git a/homeassistant/components/romy/__init__.py b/homeassistant/components/romy/__init__.py new file mode 100644 index 00000000000000..99e04d9a7686c5 --- /dev/null +++ b/homeassistant/components/romy/__init__.py @@ -0,0 +1,44 @@ +"""ROMY Integration.""" + +import romy + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, LOGGER, PLATFORMS +from .coordinator import RomyVacuumCoordinator + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """TODO.""" + + password = "" + if CONF_PASSWORD in config_entry.data: + password = config_entry.data[CONF_PASSWORD] + + new_romy = await romy.create_romy(config_entry.data[CONF_HOST], password) + + name = config_entry.data[CONF_NAME] + if name != new_romy.name: + await new_romy.set_name(name) + LOGGER.info("Settings ROMY's name to: %s", new_romy.name) + + coordinator = RomyVacuumCoordinator(hass, new_romy) + await coordinator.async_config_entry_first_refresh() + + # hass.data.setdefault(DOMAIN, {}) + # hass.data[DOMAIN][config_entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Handle removal of an entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + return unload_ok diff --git a/homeassistant/components/romy/config_flow.py b/homeassistant/components/romy/config_flow.py new file mode 100644 index 00000000000000..7eefde71d24854 --- /dev/null +++ b/homeassistant/components/romy/config_flow.py @@ -0,0 +1,128 @@ +"""Config flow for ROMY integration.""" +from __future__ import annotations + +import romy +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN, LOGGER + + +def _schema_with_defaults( + host: str = "", port: int = 8080, name: str = "" +) -> vol.Schema: + return vol.Schema( + { + vol.Required(CONF_HOST, default=host): cv.string, + vol.Optional(CONF_NAME, default=name): cv.string, + }, + ) + + +def _schema_with_defaults_and_password( + host: str = "", port: int = 8080, name: str = "", password: str = "" +) -> vol.Schema: + return vol.Schema( + { + vol.Required(CONF_HOST, default=host): cv.string, + vol.Optional(CONF_NAME, default=name): cv.string, + vol.Required(CONF_PASSWORD, default=password): vol.All(str, vol.Length(8)), + }, + ) + + +class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle config flow for ROMY.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Handle a config flow for ROMY.""" + self.discovery_schema = None + self.host: str = "" + self.name: str = "" + self.password: str = "" + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the user step.""" + errors = {} + data = self.discovery_schema or _schema_with_defaults() + + if user_input is not None: + # Validate the user input + if "host" not in user_input: + errors["host"] = "Please enter a host." + + if not errors: + ## Save the user input and finish the setup + self.host = user_input["host"] + self.name = user_input["name"] + if "password" in user_input: + self.password = user_input["password"] + + new_romy = await romy.create_romy(self.host, self.password) + + if not new_romy.is_initialized: + errors[CONF_HOST] = "wrong host" + return self.async_show_form( + step_id="user", data_schema=data, errors=errors + ) + + if not new_romy.is_unlocked: + errors[CONF_PASSWORD] = "wrong password" + return self.async_show_form( + step_id="user", data_schema=data, errors=errors + ) + + return self.async_create_entry( + title=user_input["name"], data=user_input + ) + + return self.async_show_form(step_id="user", data_schema=data, errors=errors) + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + + LOGGER.debug("Zeroconf discovery_info: %s", discovery_info) + + # extract unique id and stop discovery if robot is already added + unique_id = discovery_info.hostname.split(".")[0] + LOGGER.debug("Unique_id: %s", unique_id) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + # get ROMY's name and check if local http interface is locked + new_discovered_romy = await romy.create_romy(discovery_info.host, "") + discovery_info.name = new_discovered_romy.name + + self.context.update( + { + "title_placeholders": { + "name": f"{unique_id.split('-')[1]} ({discovery_info.host})" + }, + "configuration_url": f"http://{discovery_info.host}:{new_discovered_romy.port}", + } + ) + + if new_discovered_romy.is_unlocked: + self.discovery_schema = _schema_with_defaults( + host=discovery_info.host, + name=discovery_info.name, + ) + else: + self.discovery_schema = _schema_with_defaults_and_password( + host=discovery_info.host, + name=discovery_info.name, + password="", + ) + return await self.async_step_user() diff --git a/homeassistant/components/romy/const.py b/homeassistant/components/romy/const.py new file mode 100644 index 00000000000000..2aeb3cb1117aa9 --- /dev/null +++ b/homeassistant/components/romy/const.py @@ -0,0 +1,14 @@ +"""Constants for the ROMY integration.""" + +from datetime import timedelta +import logging + +from homeassistant.const import Platform + +# This is the internal name of the integration, it should also match the directory +# name for the integration. +DOMAIN = "romy" +ICON = "mdi:robot-vacuum" +PLATFORMS = [Platform.VACUUM] +UPDATE_INTERVAL = timedelta(seconds=5) +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/romy/coordinator.py b/homeassistant/components/romy/coordinator.py new file mode 100644 index 00000000000000..0b72dea2b0920b --- /dev/null +++ b/homeassistant/components/romy/coordinator.py @@ -0,0 +1,22 @@ +"""ROMY coordinator.""" + +from romy import RomyRobot + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, LOGGER, UPDATE_INTERVAL + + +class RomyVacuumCoordinator(DataUpdateCoordinator): + """ROMY Vacuum Coordinator.""" + + def __init__(self, hass: HomeAssistant, romy: RomyRobot) -> None: + """Setuping ROMY Vacuum Coordinator class.""" + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + self.hass = hass + self.romy = romy + + async def _async_update_data(self) -> None: + """Update ROMY Vacuum Cleaner data.""" + await self.romy.async_update() diff --git a/homeassistant/components/romy/manifest.json b/homeassistant/components/romy/manifest.json new file mode 100644 index 00000000000000..b6a8761205c40a --- /dev/null +++ b/homeassistant/components/romy/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "romy", + "name": "ROMY Vacuum Cleaner", + "codeowners": ["@xeniter"], + "config_flow": true, + "dependencies": [], + "documentation": "https://www.home-assistant.io/integrations/romy", + "iot_class": "local_polling", + "requirements": ["romy==0.0.2"], + "zeroconf": ["_aicu-http._tcp.local."] +} diff --git a/homeassistant/components/romy/strings.json b/homeassistant/components/romy/strings.json new file mode 100644 index 00000000000000..0a549159a3c055 --- /dev/null +++ b/homeassistant/components/romy/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "{name}", + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "name": "[%key:common::config_flow::data::name%]" + }, + "title": "Please provide ROMYs IP Address" + } + } + } +} diff --git a/homeassistant/components/romy/vacuum.py b/homeassistant/components/romy/vacuum.py new file mode 100644 index 00000000000000..9be47b793a162f --- /dev/null +++ b/homeassistant/components/romy/vacuum.py @@ -0,0 +1,212 @@ +"""Support for Wi-Fi enabled ROMY vacuum cleaner robots. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/vacuum.romy/. +""" + + +from collections.abc import Mapping +from typing import Any + +from romy import RomyRobot + +from homeassistant.components.vacuum import StateVacuumEntity, VacuumEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, ICON, LOGGER +from .coordinator import RomyVacuumCoordinator + +FAN_SPEED_NONE = "Default" +FAN_SPEED_NORMAL = "Normal" +FAN_SPEED_SILENT = "Silent" +FAN_SPEED_INTENSIVE = "Intensive" +FAN_SPEED_SUPER_SILENT = "Super_Silent" +FAN_SPEED_HIGH = "High" +FAN_SPEED_AUTO = "Auto" + +FAN_SPEEDS: list[str] = [ + FAN_SPEED_NONE, + FAN_SPEED_NORMAL, + FAN_SPEED_SILENT, + FAN_SPEED_INTENSIVE, + FAN_SPEED_SUPER_SILENT, + FAN_SPEED_HIGH, + FAN_SPEED_AUTO, +] + +# Commonly supported features +SUPPORT_ROMY_ROBOT = ( + VacuumEntityFeature.BATTERY + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.SEND_COMMAND + | VacuumEntityFeature.STATUS + | VacuumEntityFeature.STOP + | VacuumEntityFeature.TURN_OFF + | VacuumEntityFeature.TURN_ON + | VacuumEntityFeature.FAN_SPEED +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ROMY vacuum cleaner.""" + + coordinator: RomyVacuumCoordinator = hass.data[DOMAIN][config_entry.entry_id] + romy: RomyRobot = coordinator.romy + + device_info = { + "manufacturer": "ROMY", + "model": romy.model, + "sw_version": romy.firmware, + "identifiers": {"serial": romy.unique_id}, + } + + romy_vacuum_entity = RomyVacuumEntity(coordinator, romy, device_info) + entities = [romy_vacuum_entity] + async_add_entities(entities, True) + + +class RomyVacuumEntity(CoordinatorEntity[RomyVacuumCoordinator], StateVacuumEntity): + """Representation of a ROMY vacuum cleaner robot.""" + + def __init__( + self, + coordinator: RomyVacuumCoordinator, + romy: RomyRobot, + device_info: dict[str, Any], + ) -> None: + """Initialize the ROMY Robot.""" + super().__init__(coordinator) + self.romy = romy + self._device_info = device_info + self._attr_unique_id = self.romy.unique_id + self._state_attrs: dict[str, Any] = {} + + self._is_on = False + self._fan_speed = FAN_SPEEDS.index(FAN_SPEED_NONE) + self._fan_speed_update = False + + @property + def supported_features(self) -> VacuumEntityFeature: + """Flag vacuum cleaner robot features that are supported.""" + return SUPPORT_ROMY_ROBOT + + @property + def fan_speed(self) -> str: + """Return the current fan speed of the vacuum cleaner.""" + return FAN_SPEEDS[self.romy.fan_speed] + + @property + def fan_speed_list(self) -> list[str]: + """Get the list of available fan speed steps of the vacuum cleaner.""" + return FAN_SPEEDS + + @property + def battery_level(self) -> None | int: + """Return the battery level of the vacuum cleaner.""" + return self.romy.battery_level + + @property + def state(self) -> None | str: + """Return the state/status of the vacuum cleaner.""" + return self.romy.status + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._is_on + + @property + def name(self) -> str: + """Return the name of the device.""" + return self.romy.name + + @property + def icon(self) -> str: + """Return the icon to use for device.""" + return ICON + + @property + def device_state_attributes(self) -> Mapping[str, Any] | None: + """Return the state attributes of the device.""" + return self._state_attrs + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the vacuum on.""" + LOGGER.debug("async_turn_on") + ret = await self.romy.async_clean_start_or_continue() + if ret: + self._is_on = True + + async def async_return_to_base(self, **kwargs: Any) -> None: + """Return vacuum back to base.""" + LOGGER.debug("async_return_to_base") + ret = await self.romy.async_return_to_base() + if ret: + self._is_on = False + + # turn off robot (-> sending back to docking station) + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the vacuum off (-> send it back to docking station).""" + LOGGER.debug("async_turn_off") + await self.async_return_to_base() + + # stop robot (-> sending back to docking station) + async def async_stop(self, **kwargs: Any) -> None: + """Stop the vacuum cleaner. (-> send it back to docking station).""" + LOGGER.debug("async_stop") + await self.async_return_to_base() + + # pause robot (api call stop means stop robot where is is and not sending back to docking station) + async def async_pause(self, **kwargs: Any) -> None: + """Pause the cleaning cycle.""" + LOGGER.debug("async_pause") + ret = await self.romy.async_stop() + if ret: + self._is_on = False + + async def async_start_pause(self, **kwargs: Any) -> None: + """Pause the cleaning task or resume it.""" + LOGGER.debug("async_start_pause") + if self.is_on: + await self.async_pause() + else: + await self.async_turn_on() + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + LOGGER.debug("async_set_fan_speed to %s", fan_speed) + await self.romy.async_set_fan_speed(FAN_SPEEDS.index(fan_speed)) + + +# async def async_update(self) -> None: +# """Fetch state from the device.""" +# LOGGER.error("async_update") + +# ret, response = await self.romy_async_query("get/status") +# if ret: +# status = json.loads(response) +# self._status = status["mode"] +# self._battery_level = status["battery_level"] +# else: +# LOGGER.error( +# "ROMY function async_update -> async_query response: %s", response +# ) + +# ret, response = await self.romy_async_query("get/cleaning_parameter_set") +# if ret: +# status = json.loads(response) +# # dont update if we set fan speed currently: +# if not self._fan_speed_update: +# self._fan_speed = status["cleaning_parameter_set"] +# else: +# LOGGER.error( +# "FOMY function async_update -> async_query response: %s", response +# ) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ca81e7befaf8e5..563e16af9389e0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -371,6 +371,7 @@ "rituals_perfume_genie", "roborock", "roku", + "romy", "roomba", "roon", "rpi_power", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c91d7f888c39b2..443d426e4a4537 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4652,6 +4652,12 @@ "config_flow": true, "iot_class": "local_polling" }, + "romy": { + "name": "ROMY Vacuum Cleaner", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "roomba": { "name": "iRobot Roomba and Braava", "integration_type": "hub", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 93ccb404ae4558..6f1bf88bd815aa 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -248,6 +248,11 @@ "domain": "volumio", }, ], + "_aicu-http._tcp.local.": [ + { + "domain": "romy", + }, + ], "_airplay._tcp.local.": [ { "domain": "apple_tv", diff --git a/mypy.ini b/mypy.ini index 8628353ef6a709..36a1ee21be92c5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2412,6 +2412,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.romy.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rpi_power.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 372118195a4c0c..4a23635017749a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,6 +2281,9 @@ rocketchat-API==0.6.1 # homeassistant.components.roku rokuecp==0.18.0 +# homeassistant.components.romy +romy==0.0.2 + # homeassistant.components.roomba roombapy==1.6.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 966373d8e6dd9d..9dcdf9f507af2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1650,6 +1650,9 @@ ring_doorbell==0.7.2 # homeassistant.components.roku rokuecp==0.18.0 +# homeassistant.components.romy +romy==0.0.2 + # homeassistant.components.roomba roombapy==1.6.8 diff --git a/tests/components/romy/__init__.py b/tests/components/romy/__init__.py new file mode 100644 index 00000000000000..0e2e035f0f403d --- /dev/null +++ b/tests/components/romy/__init__.py @@ -0,0 +1 @@ +"""Tests for the ROMY integration.""" diff --git a/tests/components/romy/test_config_flow.py b/tests/components/romy/test_config_flow.py new file mode 100644 index 00000000000000..1031bc20607a77 --- /dev/null +++ b/tests/components/romy/test_config_flow.py @@ -0,0 +1,214 @@ +"""Test the ROMY config flow.""" +from unittest.mock import MagicMock, PropertyMock, patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf +from homeassistant.components.romy.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD +from homeassistant.core import HomeAssistant + + +def _create_mocked_romy(is_initialized, is_unlocked): + mocked_romy = MagicMock() + type(mocked_romy).is_initialized = PropertyMock(return_value=is_initialized) + type(mocked_romy).is_unlocked = PropertyMock(return_value=is_unlocked) + return mocked_romy + + +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the user set up form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + + assert result["errors"] is not None + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +CONFIG = {CONF_HOST: "1.2.3.4", CONF_NAME: "myROMY", CONF_PASSWORD: "12345678"} + +INPUT_CONFIG = { + CONF_HOST: CONFIG[CONF_HOST], + CONF_NAME: CONFIG[CONF_NAME], +} + + +async def test_show_user_form_with_config(hass: HomeAssistant) -> None: + """Test that the user set up form with config.""" + + mocked_romy = _create_mocked_romy( + is_initialized=True, + is_unlocked=True, + ) + + with patch( + "homeassistant.components.romy.config_flow.romy.create_romy", + return_value=mocked_romy, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=INPUT_CONFIG, + ) + + assert "errors" not in result + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +INPUT_CONFIG_HOST_MISSING = { + CONF_NAME: CONFIG[CONF_NAME], +} + + +async def test_show_user_form_with_config_with_missing_host( + hass: HomeAssistant, +) -> None: + """Test that the user set up form with config where host is missing.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=INPUT_CONFIG_HOST_MISSING, + ) + + assert "errors" in result + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +INPUT_CONFIG_WITH_PASS = { + CONF_HOST: CONFIG[CONF_HOST], + CONF_NAME: CONFIG[CONF_NAME], + CONF_PASSWORD: CONFIG[CONF_PASSWORD], +} + + +async def test_show_user_form_with_config_which_contains_password( + hass: HomeAssistant, +) -> None: + """Test that the user set up form with config which contains password.""" + + mocked_romy = _create_mocked_romy( + is_initialized=True, + is_unlocked=True, + ) + + with patch( + "homeassistant.components.romy.config_flow.romy.create_romy", + return_value=mocked_romy, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=INPUT_CONFIG_WITH_PASS, + ) + + assert "errors" not in result + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_show_user_form_with_config_which_contains_wrong_host( + hass: HomeAssistant, +) -> None: + """Test that the user set up form with config which contains password.""" + + mocked_romy = _create_mocked_romy( + is_initialized=False, + is_unlocked=False, + ) + + with patch( + "homeassistant.components.romy.config_flow.romy.create_romy", + return_value=mocked_romy, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=INPUT_CONFIG_WITH_PASS, + ) + + assert "errors" in result + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_user_form_with_config_which_contains_wrong_password( + hass: HomeAssistant, +) -> None: + """Test that the user set up form with config which contains password.""" + + mocked_romy = _create_mocked_romy( + is_initialized=True, + is_unlocked=False, + ) + + with patch( + "homeassistant.components.romy.config_flow.romy.create_romy", + return_value=mocked_romy, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=INPUT_CONFIG_WITH_PASS, + ) + + assert "errors" in result + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +# zero conf tests +################### + +DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( + host="1.2.3.4", + hostname="aicu-aicgsbksisfapcjqmqjq.local", + port=8080, + type="mock_type", + addresses="addresses", + name="myROMY", + properties={zeroconf.ATTR_PROPERTIES_ID: "aicu-aicgsbksisfapcjqmqjq"}, +) + + +async def test_zero_conf_locked_interface_robot(hass: HomeAssistant) -> None: + """Test zerconf which discovered locked robot.""" + + mocked_romy = _create_mocked_romy( + is_initialized=True, + is_unlocked=False, + ) + + with patch( + "homeassistant.components.romy.config_flow.romy.create_romy", + return_value=mocked_romy, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_zero_conf_unlocked_interface_robot(hass: HomeAssistant) -> None: + """Test zerconf which discovered already unlocked robot.""" + + mocked_romy = _create_mocked_romy( + is_initialized=True, + is_unlocked=True, + ) + + with patch( + "homeassistant.components.romy.config_flow.romy.create_romy", + return_value=mocked_romy, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM