mirror of https://github.com/home-assistant/core
Add Big Ass Fans integration (#71498)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
0584e84c30
commit
51c6a68036
|
@ -93,6 +93,9 @@ omit =
|
|||
homeassistant/components/azure_devops/const.py
|
||||
homeassistant/components/azure_devops/sensor.py
|
||||
homeassistant/components/azure_service_bus/*
|
||||
homeassistant/components/baf/__init__.py
|
||||
homeassistant/components/baf/entity.py
|
||||
homeassistant/components/baf/fan.py
|
||||
homeassistant/components/baidu/tts.py
|
||||
homeassistant/components/balboa/__init__.py
|
||||
homeassistant/components/beewi_smartclim/sensor.py
|
||||
|
|
|
@ -55,6 +55,7 @@ homeassistant.components.aseko_pool_live.*
|
|||
homeassistant.components.asuswrt.*
|
||||
homeassistant.components.automation.*
|
||||
homeassistant.components.backup.*
|
||||
homeassistant.components.baf.*
|
||||
homeassistant.components.binary_sensor.*
|
||||
homeassistant.components.bluetooth_tracker.*
|
||||
homeassistant.components.bmw_connected_drive.*
|
||||
|
|
|
@ -120,6 +120,8 @@ build.json @home-assistant/supervisor
|
|||
/homeassistant/components/azure_service_bus/ @hfurubotten
|
||||
/homeassistant/components/backup/ @home-assistant/core
|
||||
/tests/components/backup/ @home-assistant/core
|
||||
/homeassistant/components/baf/ @bdraco @jfroy
|
||||
/tests/components/baf/ @bdraco @jfroy
|
||||
/homeassistant/components/balboa/ @garbled1
|
||||
/tests/components/balboa/ @garbled1
|
||||
/homeassistant/components/beewi_smartclim/ @alemuro
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
"""The Big Ass Fans integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiobafi6 import Device, Service
|
||||
from aiobafi6.discovery import PORT
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT
|
||||
from .models import BAFData
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.FAN]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Big Ass Fans from a config entry."""
|
||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||
|
||||
service = Service(ip_addresses=[ip_address], uuid=entry.unique_id, port=PORT)
|
||||
device = Device(service, query_interval_seconds=QUERY_INTERVAL)
|
||||
run_future = device.async_run()
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT)
|
||||
except asyncio.TimeoutError as ex:
|
||||
run_future.cancel()
|
||||
raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_future)
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
data: BAFData = hass.data[DOMAIN].pop(entry.entry_id)
|
||||
data.run_future.cancel()
|
||||
|
||||
return unload_ok
|
|
@ -0,0 +1,120 @@
|
|||
"""Config flow for baf."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiobafi6 import Device, Service
|
||||
from aiobafi6.discovery import PORT
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.const import CONF_IP_ADDRESS
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.util.network import is_ipv6_address
|
||||
|
||||
from .const import DOMAIN, RUN_TIMEOUT
|
||||
from .models import BAFDiscovery
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_try_connect(ip_address: str) -> Device:
|
||||
"""Validate we can connect to a device."""
|
||||
device = Device(Service(ip_addresses=[ip_address], port=PORT))
|
||||
run_future = device.async_run()
|
||||
try:
|
||||
await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT)
|
||||
except asyncio.TimeoutError as ex:
|
||||
raise CannotConnect from ex
|
||||
finally:
|
||||
run_future.cancel()
|
||||
return device
|
||||
|
||||
|
||||
class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle BAF discovery config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the BAF config flow."""
|
||||
self.discovery: BAFDiscovery | None = None
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> FlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
properties = discovery_info.properties
|
||||
ip_address = discovery_info.host
|
||||
if is_ipv6_address(ip_address):
|
||||
return self.async_abort(reason="ipv6_not_supported")
|
||||
uuid = properties["uuid"]
|
||||
model = properties["model"]
|
||||
name = properties["name"]
|
||||
await self.async_set_unique_id(uuid, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: ip_address})
|
||||
self.discovery = BAFDiscovery(ip_address, name, uuid, model)
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Confirm discovery."""
|
||||
assert self.discovery is not None
|
||||
discovery = self.discovery
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=discovery.name,
|
||||
data={CONF_IP_ADDRESS: discovery.ip_address},
|
||||
)
|
||||
placeholders = {
|
||||
"name": discovery.name,
|
||||
"model": discovery.model,
|
||||
"ip_address": discovery.ip_address,
|
||||
}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm", description_placeholders=placeholders
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
ip_address = (user_input or {}).get(CONF_IP_ADDRESS, "")
|
||||
if user_input is not None:
|
||||
try:
|
||||
device = await async_try_connect(ip_address)
|
||||
except CannotConnect:
|
||||
errors[CONF_IP_ADDRESS] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
"Unknown exception during connection test to %s", ip_address
|
||||
)
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(device.dns_sd_uuid)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_IP_ADDRESS: ip_address}
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=device.name,
|
||||
data={CONF_IP_ADDRESS: ip_address},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_IP_ADDRESS, default=ip_address): str}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(Exception):
|
||||
"""Exception to raise when we cannot connect."""
|
|
@ -0,0 +1,19 @@
|
|||
"""Constants for the Big Ass Fans integration."""
|
||||
|
||||
DOMAIN = "baf"
|
||||
|
||||
# Most properties are pushed, only the
|
||||
# query every 5 minutes so we keep the RPM
|
||||
# sensors up to date
|
||||
QUERY_INTERVAL = 300
|
||||
|
||||
RUN_TIMEOUT = 20
|
||||
|
||||
PRESET_MODE_AUTO = "Auto"
|
||||
|
||||
SPEED_COUNT = 7
|
||||
SPEED_RANGE = (1, SPEED_COUNT)
|
||||
|
||||
ONE_MIN_SECS = 60
|
||||
ONE_DAY_SECS = 86400
|
||||
HALF_DAY_SECS = 43200
|
|
@ -0,0 +1,48 @@
|
|||
"""The baf integration entities."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiobafi6 import Device
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
||||
|
||||
class BAFEntity(Entity):
|
||||
"""Base class for baf entities."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, device: Device, name: str) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._device = device
|
||||
self._attr_unique_id = format_mac(self._device.mac_address)
|
||||
self._attr_name = name
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)},
|
||||
name=self._device.name,
|
||||
manufacturer="Big Ass Fans",
|
||||
model=self._device.model,
|
||||
sw_version=self._device.firmware_version,
|
||||
)
|
||||
self._async_update_attrs()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update attrs from device."""
|
||||
self._attr_available = self._device.available
|
||||
|
||||
@callback
|
||||
def _async_update_from_device(self, device: Device) -> None:
|
||||
"""Process an update from the device."""
|
||||
self._async_update_attrs()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add data updated listener after this object has been initialized."""
|
||||
self._device.add_callback(self._async_update_from_device)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove data updated listener after this object has been initialized."""
|
||||
self._device.remove_callback(self._async_update_from_device)
|
|
@ -0,0 +1,97 @@
|
|||
"""Support for Big Ass Fans fan."""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from aiobafi6 import OffOnAuto
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.fan import (
|
||||
DIRECTION_FORWARD,
|
||||
DIRECTION_REVERSE,
|
||||
FanEntity,
|
||||
FanEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
percentage_to_ranged_value,
|
||||
ranged_value_to_percentage,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE
|
||||
from .entity import BAFEntity
|
||||
from .models import BAFData
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SenseME fans."""
|
||||
data: BAFData = hass.data[DOMAIN][entry.entry_id]
|
||||
if data.device.has_fan:
|
||||
async_add_entities([BAFFan(data.device, data.device.name)])
|
||||
|
||||
|
||||
class BAFFan(BAFEntity, FanEntity):
|
||||
"""BAF ceiling fan component."""
|
||||
|
||||
_attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION
|
||||
_attr_preset_modes = [PRESET_MODE_AUTO]
|
||||
_attr_speed_count = SPEED_COUNT
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update attrs from device."""
|
||||
self._attr_is_on = self._device.fan_mode == OffOnAuto.ON
|
||||
self._attr_current_direction = DIRECTION_FORWARD
|
||||
if self._device.reverse_enable:
|
||||
self._attr_current_direction = DIRECTION_REVERSE
|
||||
if self._device.speed is not None:
|
||||
self._attr_percentage = ranged_value_to_percentage(
|
||||
SPEED_RANGE, self._device.speed
|
||||
)
|
||||
else:
|
||||
self._attr_percentage = None
|
||||
auto = self._device.fan_mode == OffOnAuto.AUTO
|
||||
self._attr_preset_mode = PRESET_MODE_AUTO if auto else None
|
||||
super()._async_update_attrs()
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed of the fan, as a percentage."""
|
||||
device = self._device
|
||||
if device.fan_mode != OffOnAuto.ON:
|
||||
device.fan_mode = OffOnAuto.ON
|
||||
device.speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn the fan on with a percentage or preset mode."""
|
||||
if preset_mode is not None:
|
||||
await self.async_set_preset_mode(preset_mode)
|
||||
return
|
||||
if percentage is None:
|
||||
self._device.fan_mode = OffOnAuto.ON
|
||||
return
|
||||
await self.async_set_percentage(percentage)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fan off."""
|
||||
self._device.fan_mode = OffOnAuto.OFF
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode != PRESET_MODE_AUTO:
|
||||
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||
self._device.fan_mode = OffOnAuto.AUTO
|
||||
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
self._device.reverse_enable = direction == DIRECTION_REVERSE
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"domain": "baf",
|
||||
"name": "Big Ass Fans",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||
"requirements": ["aiobafi6==0.3.0"],
|
||||
"codeowners": ["@bdraco", "@jfroy"],
|
||||
"iot_class": "local_push",
|
||||
"zeroconf": [
|
||||
{ "type": "_api._tcp.local.", "properties": { "model": "haiku*" } },
|
||||
{ "type": "_api._tcp.local.", "properties": { "model": "i6*" } }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
"""The baf integration models."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiobafi6 import Device
|
||||
|
||||
|
||||
@dataclass
|
||||
class BAFData:
|
||||
"""Data for the baf integration."""
|
||||
|
||||
device: Device
|
||||
run_future: asyncio.Future
|
||||
|
||||
|
||||
@dataclass
|
||||
class BAFDiscovery:
|
||||
"""A BAF Discovery."""
|
||||
|
||||
ip_address: str
|
||||
name: str
|
||||
uuid: str
|
||||
model: str
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "{name} - {model} ({ip_address})",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]"
|
||||
}
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to setup {name} - {model} ({ip_address})?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"ipv6_not_supported": "IPv6 is not supported.",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"ipv6_not_supported": "IPv6 is not supported."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"flow_title": "{name} - {model} ({ip_address})",
|
||||
"step": {
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to setup {name} - {model} ({ip_address})?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP Address"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ FLOWS = {
|
|||
"axis",
|
||||
"azure_devops",
|
||||
"azure_event_hub",
|
||||
"baf",
|
||||
"balboa",
|
||||
"blebox",
|
||||
"blink",
|
||||
|
|
|
@ -42,6 +42,20 @@ ZEROCONF = {
|
|||
"domain": "apple_tv"
|
||||
}
|
||||
],
|
||||
"_api._tcp.local.": [
|
||||
{
|
||||
"domain": "baf",
|
||||
"properties": {
|
||||
"model": "haiku*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"domain": "baf",
|
||||
"properties": {
|
||||
"model": "i6*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"_api._udp.local.": [
|
||||
{
|
||||
"domain": "guardian"
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -368,6 +368,17 @@ no_implicit_optional = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.baf.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.binary_sensor.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -121,6 +121,9 @@ aioasuswrt==1.4.0
|
|||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==1.3.5
|
||||
|
||||
# homeassistant.components.baf
|
||||
aiobafi6==0.3.0
|
||||
|
||||
# homeassistant.components.aws
|
||||
aiobotocore==2.1.0
|
||||
|
||||
|
|
|
@ -108,6 +108,9 @@ aioasuswrt==1.4.0
|
|||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==1.3.5
|
||||
|
||||
# homeassistant.components.baf
|
||||
aiobafi6==0.3.0
|
||||
|
||||
# homeassistant.components.aws
|
||||
aiobotocore==2.1.0
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Big Ass Fans integration."""
|
|
@ -0,0 +1,158 @@
|
|||
"""Test the baf config flow."""
|
||||
import asyncio
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.baf.const import DOMAIN
|
||||
from homeassistant.const import CONF_IP_ADDRESS
|
||||
from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_ABORT,
|
||||
RESULT_TYPE_CREATE_ENTRY,
|
||||
RESULT_TYPE_FORM,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_form_user(hass):
|
||||
"""Test we get the user form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch(
|
||||
"homeassistant.components.baf.config_flow.Device.async_wait_available",
|
||||
), patch(
|
||||
"homeassistant.components.baf.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_IP_ADDRESS: "127.0.0.1"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result2["title"] == "127.0.0.1"
|
||||
assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch(
|
||||
"homeassistant.components.baf.config_flow.Device.async_wait_available",
|
||||
side_effect=asyncio.TimeoutError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_IP_ADDRESS: "127.0.0.1"},
|
||||
)
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_unknown_exception(hass):
|
||||
"""Test we handle unknown exceptions."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch(
|
||||
"homeassistant.components.baf.config_flow.Device.async_wait_available",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_IP_ADDRESS: "127.0.0.1"},
|
||||
)
|
||||
|
||||
assert result2["type"] == RESULT_TYPE_FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_zeroconf_discovery(hass):
|
||||
"""Test we can setup from zeroconf discovery."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
host="127.0.0.1",
|
||||
addresses=["127.0.0.1"],
|
||||
hostname="mock_hostname",
|
||||
name="testfan",
|
||||
port=None,
|
||||
properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.baf.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "My Fan"
|
||||
assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_zeroconf_updates_existing_ip(hass):
|
||||
"""Test we can setup from zeroconf discovery."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_IP_ADDRESS: "127.0.0.2"}, unique_id="1234"
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
host="127.0.0.1",
|
||||
addresses=["127.0.0.1"],
|
||||
hostname="mock_hostname",
|
||||
name="testfan",
|
||||
port=None,
|
||||
properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data[CONF_IP_ADDRESS] == "127.0.0.1"
|
||||
|
||||
|
||||
async def test_zeroconf_rejects_ipv6(hass):
|
||||
"""Test zeroconf discovery rejects ipv6."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
host="fd00::b27c:63bb:cc85:4ea0",
|
||||
addresses=["fd00::b27c:63bb:cc85:4ea0"],
|
||||
hostname="mock_hostname",
|
||||
name="testfan",
|
||||
port=None,
|
||||
properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "ipv6_not_supported"
|
Loading…
Reference in New Issue