hass-core/tests/components/rabbitair/test_config_flow.py
Alexander Somov d754ea7e22
Add new Rabbit Air integration (#66130)
* Add new Rabbit Air integration

* Remove py.typed file

It is not needed and was just accidentally added to the commit.

* Enable strict type checking for rabbitair component

Keeping the code fully type hinted is a good idea.

* Add missing type annotations

* Remove translation file

* Prevent data to be added to hass.data if refresh fails

* Reload the config entry when the options change

* Add missing type parameters for generics

* Avoid using assert in production code

* Move zeroconf to optional dependencies

* Remove unnecessary logging

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Remove unused keys from the manifest

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Replace property with attr

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Allow to return None for power

The type of the is_on property now allows this.

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Remove unnecessary method call

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Update the python library

The new version properly re-exports names from the package root.

* Remove options flow

Scan interval should not be part of integration configuration. This was
the only option, so the options flow can be fully removed.

* Replace properties with attrs

* Remove multiline ternary operator

* Use NamedTuple for hass.data

* Remove unused logger variable

* Move async_setup_entry up in the file

* Adjust debouncer settings to use request_refresh

* Prevent status updates during the cooldown period

* Move device polling code to the update coordinator

* Fix the problem with the switch jumping back and forth

The UI seems to have a timeout of 2 seconds somewhere, which is just a
little bit less than what we normally need to get an updated state. So
the power switch would jump to its previous state and then immediately
return to the new state.

* Update the python library

The new version fixes errors when multiple requests are executed
simultaneously.

* Fix incorrect check for pending call in debouncer

This caused the polling to stop.

* Fix tests

* Update .coveragerc to exclude new file.
* Remove test for Options Flow.

* Update the existing entry when device access details change

* Add Zeroconf discovery step

* Fix tests

The ZeroconfServiceInfo constructor now requires one more argument.

* Fix typing for CoordinatorEntity

* Fix signature of async_turn_on

* Fix depreciation warnings

* Fix manifest formatting

* Fix warning about debouncer typing

relates to 5ae5ae5392

* Wait for config entry platform forwards

* Apply some of the suggested changes

* Do not put the MAC address in the title. Use a fixed title instead.
* Do not format the MAC to use as a unique ID.
* Do not catch exceptions in _async_update_data().
* Remove unused _entry field in the base entity class.
* Use the standard attribute self._attr_is_on to keep the power state.

* Store the MAC in the config entry data

* Change the order of except clauses

OSError is an ancestor class of TimeoutError, so TimeoutError should be
handled first

* Fix depreciation warnings

* Fix tests

The ZeroconfServiceInfo constructor arguments have changed.

* Fix DeviceInfo import

* Rename the method to make it clearer what it does

* Apply suggestions from code review

* Fix tests

* Change speed/mode logic to use is_on from the base class

* A zero value is more appropriate than None

since None means "unknown", but we actually know that the speed is zero
when the power is off.

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2024-01-05 16:34:28 +01:00

210 lines
6.5 KiB
Python

"""Test the RabbitAir config flow."""
from __future__ import annotations
import asyncio
from collections.abc import Generator
from ipaddress import ip_address
from unittest.mock import Mock, patch
import pytest
from rabbitair import Mode, Model, Speed
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.rabbitair.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MAC
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.device_registry import format_mac
TEST_HOST = "1.1.1.1"
TEST_NAME = "abcdef1234_123456789012345678"
TEST_TOKEN = "0123456789abcdef0123456789abcdef"
TEST_MAC = "01:23:45:67:89:AB"
TEST_FIRMWARE = "2.3.17"
TEST_HARDWARE = "1.0.0.4"
TEST_UNIQUE_ID = format_mac(TEST_MAC)
TEST_TITLE = "Rabbit Air"
ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo(
ip_address=ip_address(TEST_HOST),
ip_addresses=[ip_address(TEST_HOST)],
port=9009,
hostname=f"{TEST_NAME}.local.",
type="_rabbitair._udp.local.",
name=f"{TEST_NAME}._rabbitair._udp.local.",
properties={"id": TEST_MAC.replace(":", "")},
)
@pytest.fixture(autouse=True)
def use_mocked_zeroconf(mock_async_zeroconf):
"""Mock zeroconf in all tests."""
@pytest.fixture
def rabbitair_connect() -> Generator[None, None, None]:
"""Mock connection."""
with patch("rabbitair.UdpClient.get_info", return_value=get_mock_info()), patch(
"rabbitair.UdpClient.get_state", return_value=get_mock_state()
):
yield
def get_mock_info(mac: str = TEST_MAC) -> Mock:
"""Return a mock device info instance."""
mock_info = Mock()
mock_info.mac = mac
return mock_info
def get_mock_state(
model: Model | None = Model.A3,
main_firmware: str | None = TEST_HARDWARE,
power: bool | None = True,
mode: Mode | None = Mode.Auto,
speed: Speed | None = Speed.Low,
wifi_firmware: str | None = TEST_FIRMWARE,
) -> Mock:
"""Return a mock device state instance."""
mock_state = Mock()
mock_state.model = model
mock_state.main_firmware = main_firmware
mock_state.power = power
mock_state.mode = mode
mock_state.speed = speed
mock_state.wifi_firmware = wifi_firmware
return mock_state
@pytest.mark.usefixtures("rabbitair_connect")
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert not result["errors"]
with patch(
"homeassistant.components.rabbitair.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: TEST_HOST,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == TEST_TITLE
assert result2["data"] == {
CONF_HOST: TEST_HOST,
CONF_ACCESS_TOKEN: TEST_TOKEN,
CONF_MAC: TEST_MAC,
}
assert result2["result"].unique_id == TEST_UNIQUE_ID
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("error_type", "base_value"),
[
(ValueError, "invalid_access_token"),
(OSError, "invalid_host"),
(asyncio.TimeoutError, "timeout_connect"),
(Exception, "cannot_connect"),
],
)
async def test_form_cannot_connect(
hass: HomeAssistant, error_type: type[Exception], base_value: str
) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert not result["errors"]
with patch(
"rabbitair.UdpClient.get_info",
side_effect=error_type,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: TEST_HOST,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": base_value}
async def test_form_unknown_error(hass: HomeAssistant) -> None:
"""Test we handle unknown error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert not result["errors"]
with patch(
"homeassistant.components.rabbitair.config_flow.validate_input",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: TEST_HOST,
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "unknown"}
@pytest.mark.usefixtures("rabbitair_connect")
async def test_zeroconf_discovery(hass: HomeAssistant) -> None:
"""Test zeroconf discovery setup flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZEROCONF_DATA
)
assert result["type"] == FlowResultType.FORM
assert not result["errors"]
with patch(
"homeassistant.components.rabbitair.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: TEST_NAME + ".local",
CONF_ACCESS_TOKEN: TEST_TOKEN,
},
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.CREATE_ENTRY
assert result2["title"] == TEST_TITLE
assert result2["data"] == {
CONF_HOST: TEST_NAME + ".local",
CONF_ACCESS_TOKEN: TEST_TOKEN,
CONF_MAC: TEST_MAC,
}
assert result2["result"].unique_id == TEST_UNIQUE_ID
assert len(mock_setup_entry.mock_calls) == 1
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZEROCONF_DATA
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"